1. Separate the enquiry from its analytics signal
A contact form is often the most useful conversion on a small business website. The business needs the enquiry’s contact details to respond. Analytics usually needs only to know that an enquiry was accepted. Treating those as two separate data flows makes the integration easier to reason about.
Your own form service receives the name, email and message. WhoVisited receives a short event such as contact_request_accepted. Do not pass FormData, input values or the server’s full response into the tracking call. This tutorial adds a measurement signal to a form you already operate; WhoVisited is not a form backend, email delivery service or CRM.
The event describes an accepted request, not a qualified lead, a completed sale or guaranteed email delivery. Pick its name to match the stage your application can actually confirm. If your service only queues requests for later processing, say that in your success interface and measurement notes.
2. Define the backend success condition first
A click on Submit and a browser submit event both happen before you know whether the request succeeded. Invalid fields, spam filtering, network failures or service errors may still prevent acceptance. Place analytics after the business operation rather than at the button.
Response.ok indicates an HTTP status in the 200–299 range. It does not by itself prove that your service accepted an enquiry. Some application APIs return an error result inside a successful HTTP response. Inspect the contract of your existing endpoint and define one explicit success condition.
In the example below, your adapter must resolve to { accepted: true } only after your form service confirms acceptance. It should reject on transport failures and return { accepted: false } on known validation or business failures. This is an example integration contract you implement around your own endpoint, not an API supplied by WhoVisited.
Keep request acceptance and later delivery distinct. If an email worker fails after the application accepts a request, its retry and alerting belong to that service. A browser analytics event cannot confirm the eventual inbox delivery.
3. Add a minimal client-side completion helper
The helper below takes the existing business operation as a callback. It prevents overlapping submissions in this particular handler instance and records at most one successful completion during its lifetime. Create it once for the form instance; do not recreate it on every render or button click.
function createContactSubmitter(sendRequest) {
let busy = false;
let completed = false;
return async function submit(formData) {
if (busy || completed) return { status: "ignored" };
busy = true;
try {
const result = await sendRequest(formData);
if (result?.accepted !== true) return { status: "rejected" };
completed = true;
// Send only an event name, never the enquiry or response object.
try {
window.whovisited?.track("contact_request_accepted");
} catch {
// Analytics must not change a confirmed business success.
}
return { status: "accepted" };
} finally {
busy = false;
}
};
}Invoke submit(formData) from your existing form handler after client validation, then show the appropriate success or error state. FormData is forwarded only to the callback responsible for your own form service. The analytics call contains no properties and never receives that object.
A rejected business result permits a later retry. A thrown transport error also releases the busy state, and your UI should catch it and display a useful error. After success, repeated calls are ignored. If your product intentionally allows a new enquiry, explicitly start a new form instance rather than resetting the helper silently.
This guard does not provide server idempotency. A reload, another tab or an uncertain network response can still lead to duplicate business requests. Add duplicate protection to your form backend according to its own contract; do not treat an analytics session count as a database constraint.
4. Check the surrounding page for accidental disclosure
A minimal event is one part of the boundary. WhoVisited’s tracker also includes the page path and page title, and supports explicit custom properties. Avoid rendering enquiry contents in the title or embedding email addresses and names into path segments. Use a generic success path such as /contact/thanks.
The tracker removes query strings and fragments from the page URL it sends, but that does not make personal data in URLs safe throughout your application. Browser history, your server logs and other tools may still see them. Keep contact details in the intended form submission channel instead.
- Do not turn all form fields into analytics properties with Object.fromEntries(formData).
- Do not send the backend response, request ID containing user details, or a message excerpt as an event property.
- Use a fixed event name instead of constructing it from the visitor’s name or email.
- Use synthetic test values and redact tokens and payloads before sharing debugging screenshots.
The current property validation limits types and lengths; it does not automatically remove personal information from a string. Data minimization depends on what your integration chooses to send.
5. Keep business success independent of tracking availability
The helper uses optional chaining when the tracker API is absent. That allows a successful contact request to stay successful even if analytics was blocked or had not loaded. It does not queue a missing call for later or confirm delivery to WhoVisited.
The track API does not return a delivery receipt. An initialized tracker can still decline collection because consent has not been granted, a browser privacy signal is active, collection is disabled or the workspace cannot ingest events. The helper’s accepted status means your form service accepted the request, not that analytics recorded it.
If analytics is required to wait for consent on your website, connect the existing consent manager to the tracker and test both paths. Never make form submission conditional on agreeing to analytics merely to fill a report. For a missing event, use the troubleshooting reference below and inspect the collection request separately from the contact request.
6. Match the custom event to a precise goal
In WhoVisited Settings, create an event goal for the correct website with the exact name contact_request_accepted. Then select that website, goal and date range in Growth. The name uses letters and underscores, within the tracker’s event naming rules.
WhoVisited checks whether a retained matching event exists in the same session and counts that session once for the goal. Two accepted enquiries in one session are not two goal-converted sessions. Use your form database for total requests and follow-up status; use the Growth report to compare the sessions that reached acceptance.
A success-page goal is an alternative when your application uses a dedicated confirmation page. It measures a visit to that path, so a bookmark, reload or direct visit can differ from a newly accepted enquiry. Choose the custom event when you can reliably place it after the backend’s result, and avoid adding both as if they were separate business successes.
7. Validate outcomes before reviewing campaign performance
- Validation failure: the business operation returns accepted: false, the UI shows the error and the tracking callback is not called.
- Transport failure: the promise rejects, the handler becomes available for retry and no success event is generated.
- Confirmed success: your adapter returns accepted: true and the tracker receives exactly the fixed event name without form properties.
- Double submission: a second call while the first is pending is ignored; a second call after success is also ignored by this instance.
- Tracker absent or throwing: the accepted request remains accepted; analytics must not turn it into a user-visible business failure.
After the isolated handler checks, run one controlled test through your deployed website with synthetic contact information. Confirm the form service accepted it, then inspect the WhoVisited events response and the matching visitor journey. Select the goal in Growth and verify the session is represented.
Use a consistently tagged incoming campaign link if you are comparing placements. Keep the same form and goal across the comparison, and allow enough recorded sessions before drawing conclusions. This setup can show which observable sessions reached request acceptance. It cannot tell you that every enquiry became a customer or that every visitor was measurable.