DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Five of our ten analytics event groups were never called, so the insights said no data forever

Our analytics event file used to define around 50 custom events across ten groups. Five entire groups, payment, feature, engagement, error and marketing, were never called from anywhere in the application. Not called rarely. Not called on a path nobody took. Never called, from any file.

The symptom was a set of insights that read "no data" and stayed that way. The natural conclusion is that traffic is too low, or the integration is broken, or the chart needs a wider window. The actual cause was that the events did not exist, so no amount of waiting could ever populate them. An event name in a dashboard looks exactly like an event name in code.

It now defines 11:

game:started            onboarding:started       review_prompt:shown
game:completed          onboarding:completed     review_prompt:clicked
user:signed_out         onboarding:skipped       review_prompt:dismissed
payment:checkout_started onboarding:deleted
Enter fullscreen mode Exit fullscreen mode

The bar for adding one

The rule we wrote into the top of the file is that there has to be a decision we would make differently depending on the answer. That is a higher bar than "this would be interesting", and it is the only bar that stops a catalogue from growing back.

Everything that fails it usually belongs somewhere else:

  • errors belong in Sentry, where they arrive with a stack trace;
  • revenue belongs in Stripe, which is the system of record anyway;
  • per-user history belongs in our own database, because that is what the product reads to show somebody their own progress.

A custom event that duplicates one of those is not just noise, it is a second number that can disagree with the first.

The funnel that needed no events at all

The clearest deletion was the auth group: signup_started, signup_completed, signup_failed and their sign-in twins. The signup flow is three routes, /signup, then /signup/success, then /dashboard, and the automatic pageview pair already records all three. A funnel over pageviews needs no code, and more importantly it cannot drift out of sync with the routes. Hand-fired funnel events can, and the failure mode is silent: the route changes, the event keeps firing from the old place, and the funnel quietly measures something else.

The one auth event we kept is sign-out, and it is kept for a reason that has nothing to do with counting sign-outs:

export function trackSignOut(): void {
  capture('user:signed_out');
  resetIdentity();
}
Enter fullscreen mode Exit fullscreen mode

Without the reset, the next person to sign in on a shared browser inherits the previous person's distinct_id, and two people's activity merges into one. On a product used on shared and library machines that is not an edge case, and it corrupts every per-person metric rather than just this one.

What the events are allowed to carry

Identification sends exactly one property:

export function identifyUser(userId: string, planTier?: string): void {
  identify(userId, { plan_tier: planTier ?? 'free' });
}
Enter fullscreen mode Exit fullscreen mode

plan_tier is there because it is the one property worth breaking every other metric down by. Email is deliberately not sent.

The onboarding wizard is the interesting case, because it collects what somebody is applying for, which can be free text. Nothing free text is sent. The events carry taxonomy ids, plus two derived booleans:

capture('onboarding:completed', {
  employer_id: answers.employerId,
  employer_is_listed:
    answers.employerId !== null && !['other', 'undecided'].includes(answers.employerId),
  role_id: answers.roleId,
  role_is_listed: answers.roleId !== null && answers.roleId !== 'other',
  variant: answers.variant,
});
Enter fullscreen mode Exit fullscreen mode

Those booleans answer the only question we would act on, which is whether our list of employers covers what people actually type. "How many people picked an employer we do not list" is a product decision. The company somebody typed is not ours to collect to answer it.

One last constraint, which is a boring one that saves real pain: the naming stayed as category:action rather than being modernised, because the events that survived have history, and renaming them would have orphaned every chart built on them.

See it

Open cogniprep.app with the Network tab open and filter for ingest. You will see a handful of requests, all of them on our own domain:

/ingest/array/<project key>/config.js
/ingest/static/1.427.2/web-vitals-with-attribution.js
/ingest/static/1.427.2/surveys.js
/ingest/i/v0/e/
Enter fullscreen mode Exit fullscreen mode

The last one is where events go. There is no third-party analytics domain in that list, because the SDK is served and posted through a first-party path. Two honest notes if you go looking: the SDK is not exposed as window.posthog, so poking at it from the console will not work, and the event payloads are compressed, so the Network tab shows you that events are sent rather than which ones. The count is the part worth taking away anyway. Eleven events, and every one of them answers a question we would act on.

Top comments (1)

Collapse
 
omyvnss profile image
Om Yaduvanshi •

the sign-out event is my favourite detail. it survives the cull not because anyone counts sign-outs, but because resetidentity stops two people on a shared machine merging into one person in the data. the rarest kind of event, one kept purely for hygiene.