Why a successful action can happen before analytics is ready
A user submits a signup form, your server creates the account, and your success handler calls `window.whovisited.track("signup_completed")`. The signup succeeds, but no conversion event appears. One cause is timing: an asynchronous analytics script can still be downloading when the application code reaches the success handler.
Next.js recommends `next/script` for third-party scripts such as analytics. With the `afterInteractive` strategy, the script loads after some hydration has occurred. The browser's `async` script attribute also means the browser executes the script when its download is ready, not at a fixed point relative to unrelated application work. Until the tracker runs, `window.whovisited` does not exist.
Optional chaining avoids a JavaScript exception, but `window.whovisited?.track(...)` silently does nothing when the global is missing. That can be a sensible fail-open choice for the signup experience, but it is not a reliable event-delivery strategy. Queueing a small number of allowed events in memory can bridge this initialization race.
Add a bounded in-memory queue in a Client Component
Next.js Script's `onReady` callback runs after the script loads and again when the component mounts after client-side navigation. Event handlers belong in a Client Component, so keep the queue and the Script component together. This example holds at most 20 synthetic event descriptions in the current page's memory; it does not write them to browser storage.
// app/components/tracker-loader.tsx
"use client";
import Script from "next/script";
type QueuedEvent = {
name: string;
properties?: Record<string, string | number | boolean | null>;
};
declare global {
interface Window {
whovisited?: {
track: (name: string, properties?: QueuedEvent["properties"]) => void;
};
}
}
const pending: QueuedEvent[] = [];
const maxPending = 20;
function flushPendingEvents() {
const tracker = window.whovisited;
if (!tracker) return;
for (const event of pending.splice(0)) {
tracker.track(event.name, event.properties);
}
}
export function trackWhenReady(event: QueuedEvent) {
if (typeof window === "undefined") return false;
const tracker = window.whovisited;
if (tracker) {
flushPendingEvents();
tracker.track(event.name, event.properties);
return true;
}
if (pending.length >= maxPending) return false;
pending.push(event);
return true;
}
export function TrackerLoader({ siteId }: { siteId: string }) {
return (
<Script
src="https://whovisited.net/tracker.js"
data-site-id={siteId}
strategy="afterInteractive"
onReady={flushPendingEvents}
onError={() => { pending.length = 0; }}
/>
);
}Render `<TrackerLoader siteId="YOUR_SITE_ID" />` from the root layout, and import `trackWhenReady` into the client code that handles the business action. The helper drains earlier queued items before sending a later event, so a success event does not jump ahead of events that were waiting for initialization. If the script fails to load, the example discards its pending list when Next.js reports the error.
Queue a confirmed outcome, not a button click
Call the helper after the server confirms the action you want to measure. A click is an interaction; it does not establish that an account was created or a form was accepted. For example, the client submit handler can call the tracker helper after receiving a successful response from your existing signup endpoint:
const result = await createAccount(formData);
if (result.ok) {
trackWhenReady({ name: "signup_completed" });
router.push("/welcome");
}Use an event name that matches the goal you configured in WhoVisited. Custom event names are case-sensitive, begin with a letter, and may contain letters, digits, or underscores up to 64 characters. Do not send names, email addresses, passwords, form contents, access tokens, or account IDs as properties. The goal should describe an outcome that your application actually confirmed.
If the queue is already full, `trackWhenReady` returns `false`; ignore that result for the business flow. Analytics must never decide whether the user sees a successful signup screen. Keep duplicate-submit prevention and account creation idempotency in your application, because a queued event is not a substitute for either.
Keep consent and delivery limits explicit
This helper coordinates script readiness only. It does not grant consent, override Do Not Track or Global Privacy Control, make a blocked script load, or guarantee that the event reaches the server. If the website requires analytics consent, do not enqueue pre-consent actions for later replay. After the visitor grants consent, initialize the tracker using your consent manager and only then queue events that are permitted to be collected.
The queue exists only in the current tab's JavaScript memory. A reload, navigation that unloads the document, browser shutdown, or script error can discard pending events. The tracker itself batches events and makes network requests separately; the helper receives no acknowledgement that an event was stored. Treat it as protection against one initialization race, not as a durable message queue or an accounting ledger.
The backend can also reject collection when the origin is not allowed, the website is disabled, the plan is inactive, a feature is unavailable, or a rate or monthly quota is reached. A success in your application is still a success even when analytics is unavailable. Use your own database and server logs for authoritative business totals.
Test both the timing fix and its failure path
- In a test environment, throttle the browser's network in DevTools and submit a synthetic signup before the tracker script finishes loading. Confirm that the signup proceeds and the custom event is queued in memory.
- Restore normal network conditions and confirm that the tracker initializes and sends the event. In the Network panel, inspect the bootstrap request and the later events request; a script download alone does not prove ingestion.
- After the event appears in the visitor journey, check that the configured custom-event goal uses the exact event name and that the same session contains the conversion.
- Block the tracker request or use a test page with a failing script URL. Confirm that the signup still succeeds, pending events are discarded on load error, and no analytics code throws in the application.
- If consent is required, repeat with consent withheld and granted. Confirm that the queue does not replay actions from before consent and that the consent manager remains responsible for the decision.
For installation and a complete conversion walkthrough, see the [Next.js signup tracking guide](https://whovisited.net/blog/nextjs-signup-conversion-tracking). For an event that still does not appear, follow the [tracker debugging checklist](https://whovisited.net/blog/analytics-tracker-loads-but-no-events). Next.js's [Script component reference](https://nextjs.org/docs/app/api-reference/components/script) documents `afterInteractive`, `onReady`, and the Client Component restriction for event handlers.