DEV Community

Nabeel Hassan
Nabeel Hassan

Posted on Originally published at nullstud.io

Ten Events and Three Numbers: Instrumenting an MVP Like an Engineer

Most MVPs launch in one of two states. Either there is no analytics at all, or someone dropped in the default snippet and it has been dutifully recording page views and button clicks that nobody has opened since launch week.

Both end the same way. The founder asks "is it working?" and the honest answer is a feeling.

That question is an engineering problem even if nobody frames it as one. Analytics breaks for the same reasons other systems break: vague contracts, unreliable clients, identity bugs and data nobody validates. Here is how I treat instrumentation on a first version.

Start from decisions, not events

Do not open an analytics tool and ask "what should we track?" Ask what you will decide in the next three months, and what data would change your mind. For a typical MVP:

  • Is the core loop working? Do people who sign up reach the moment where the product does its job?
  • Where do they drop off? Which step between install and that moment loses the most people?
  • Do they come back? Of the people who got value once, how many return without a nudge?
  • Which channel is worth doubling down on? Do users from one source behave differently?

If an event maps to no decision, it is noise you pay for later in cleanup, event-volume pricing and confusion. Same instinct as logging: a log line nobody reads is just cost.

Activation is a spec, so write it like one

Activation is the moment a new user first gets the value the product promises. A few from products we build and run at Null Studio:

  • LectureNotes AI records lectures and turns them into summaries and outlines. Signing up is not activation. Activation is a student opening their first generated summary of a real lecture.
  • Lifemaxxing AI builds personalized self-improvement plans. Activation is closer to receiving the plan and logging the first piece of progress against it, not finishing onboarding.
  • Fyuel, an accounting platform for the fuel trade, is a business tool. Activation there is a company getting real customers, suppliers and transactions into the system, because an empty ledger has delivered nothing.

None of these are "created an account." Write the moment in one sentence, get the team to agree, then instrument every step that leads to it. If the team cannot agree on the sentence, analytics just found you a product problem for free.

The tracking plan is your event contract

A tracking plan lists every event the product sends, when it fires and what it carries. I treat it as an API contract between the product and whoever reads the data later. Ten to twenty events is plenty. Each row needs:

  • A name in one consistent style: past tense, snake_case, like summary_viewed or plan_created.
  • When it fires, precisely. "When the summary screen finishes loading with content" beats "when the user sees the summary." The second one will be implemented three different ways by three different people.
  • Properties, where it fires (device or server) and which decision it supports.

If your stack is TypeScript, you can make the plan enforce itself instead of living only in a spreadsheet. This is illustrative, not lifted from a specific codebase, but it is the shape I reach for:

// Illustrative: the tracking plan as a type, so a typo is a compile error
type Events = {
  signup_completed: { source: "organic" | "paid" | "referral" };
  recording_uploaded: { durationSec: number; platform: "ios" | "android" | "web" };
  summary_viewed: { lectureId: string; isFirst: boolean };
  plan_created: { planType: string };
};

export function track<E extends keyof Events>(
  event: E,
  props: Events[E],
  userId: string
) {
  analytics.capture({ distinctId: userId, event, properties: props });
}

// track("summary_veiwed", ...) fails to compile.
// track("summary_viewed", { lectureId }) fails too: isFirst is missing.
Enter fullscreen mode Exit fullscreen mode

The point is not the wrapper. The point is that the event list now lives in the repo, shows up in code review and cannot drift silently from what the dashboard expects.

Send outcomes from the server

Events that record something the product did, like a payment succeeding, a summary being generated or an invoice being saved, belong on the backend. Device-side tracking gets blocked, lost offline and duplicated when a mobile client retries. If your activation event fires from the client, your activation rate is partly a measurement of how many users run an ad blocker. The server knows the summary was generated. Let the server say so.

Keep device events for what only the interface knows, like which screen someone abandoned. And name everything for the person who joins in six months: btn_click_3 tells them nothing, checkout_started with a plan property tells them everything.

Identity is the bug that corrupts your best data

Analytics breaks quietly when one person looks like three users. Someone browses anonymously, signs up on the web, then logs in on their phone. Without care, that is three distinct IDs and a funnel that says your conversion is a third of what it really is.

Decide early how you will:

  • Merge anonymous activity into the account at sign-up, so the steps before registration still count toward that person's journey.
  • Recognize one person across web and mobile, using your own user ID rather than a device ID.
  • Carry an account or organization ID if you sell to businesses. For a B2B tool like Fyuel, the real question is often whether a company activated, not whether one person did.

The nasty part is timing. Fix identity after launch and the data you cannot trust is from your launch weeks, exactly the period you most wanted to read.

Three numbers, reviewed every week

Early on, three numbers do almost all the work.

Activation rate. Of the people who signed up this week, how many reached the activation moment? If it is low, walk the funnel to find the step where people stop.

Retention by cohort. Group users by signup week and track how many come back in the following weeks. A growing total user count can hide the fact that every new cohort leaves as fast as the last one. The cohort chart cannot hide that.

The core value action. The one repeated action that means the product is delivering: summaries viewed, plans updated, transactions recorded. Usually a better health signal than DAU, because people open apps without getting anything out of them.

Review those three every week with a named owner. A metric nobody looks at is not a metric. It is storage. And pair them with a few conversations with users who activated and users who dropped off: with dozens or hundreds of users, the why matters more than the what.

Privacy is part of the schema

Decide before launch what you collect, how long you keep it and how users opt out or delete it. Never send sensitive content, like the text of someone's notes or financial details, into third-party analytics. The typed schema helps again: if there is no noteText property in the plan, nobody casually adds one. On mobile, this also has to match the privacy disclosures you give Apple and Google. For AI features, the same care applies to prompts and outputs, which I covered in Nobody Opened Your Product to Chat With It.

Do not build your own pipeline yet

I feel the pull to build a custom event pipeline too. Resist it until you know the product has users. The effort belongs in the plan, server-side events and identity, not charts. A few rules:

  • One tool, not three. Three tools with slightly different numbers produce arguments, not decisions.
  • Own the data. The account should be in the company's name, with raw event export available. Same principle as Your README Is a Lie Until Someone Else Runs It: if you cannot hand it over, you do not really own it.
  • Check the pricing curve. Many tools bill by event volume, one more reason not to auto-capture everything.
  • Test events like features. They break when screens change and nothing fails loudly.

It is a few days, not a project

Instrumentation is a few days of work inside the MVP build, far cheaper then than retrofitted later. When we scope a first version, the tracking plan is written with the feature list, so activation is agreed before development starts.

My own product taught me why. When LectureNotes AI went viral on Instagram, the reach was the easy part. The real work was the path from view to paid: getting a student to their first good set of notes fast, and keeping them through the next exam season. I wrote that up in 15 Million Views, 10k Users, $1k MRR.

The original, longer version of this guide is on the Null Studio blog: Product analytics for an MVP.

What is the one event in your current project you would be most afraid to discover was firing wrong?

Top comments (13)

Collapse
 
omyvnss profile image
Om Yaduvanshi •

the identity section is the one i'd move earlier. everything else in the plan assumes one person is one row, and if that assumption breaks after launch, the launch data is the part you can't trust or repair. decisions-first is good; identity-first is the precondition.

Collapse
 
nabeelbaghoor profile image
Nabeel Hassan •

That's a fair reordering, and I think you're right. Every other number in the plan quietly assumes one person is one row, so if identity breaks, activation and retention break with it, and you can't backfill a merge you never recorded. In the post it reads like one section among several, but it's really the precondition for all of them. If I rewrote it, I'd open with: pick the user ID, decide the exact moment anonymous becomes known, and test the merge before the first event ships. Thanks for pushing on that.

Collapse
 
omyvnss profile image
Om Yaduvanshi •

glad that landed. the "one person one row" assumption is where every dashboard i've seen goes sideways first, and everything downstream quietly inherits the merge bug. opening with "pick the user id, decide the exact moment anonymous becomes known" is a much stronger first page.

Thread Thread
 
nabeelbaghoor profile image
Nabeel Hassan •

Agreed, and "everything downstream quietly inherits the merge bug" is a better one-line summary of the problem than anything I wrote. Thanks for the back and forth, it will shape how I write the next version of this.

Thread Thread
 
omyvnss profile image
Om Yaduvanshi •

appreciate that. for the next version, the line i'd add: the merge bug doesn't just corrupt the past, it taxes every query you write after it, because each one now has to hedge against double-counting.

Thread Thread
 
nabeelbaghoor profile image
Nabeel Hassan •

That is a great way to put it. The corrupted rows are a one-time cost, but the hedging is permanent: every query after it carries a DISTINCT or a dedupe join that someone has to remember, and the one person who forgets ships a number that is quietly wrong. Which is the best argument I know for fixing identity before launch rather than after.

Thread Thread
 
omyvnss profile image
Om Yaduvanshi •

that dedupe join point is the one that stays with me. once the workaround is in every query, nobody remembers it was a workaround, so the fix never gets scheduled.

Thread Thread
 
nabeelbaghoor profile image
Nabeel Hassan •

Exactly. A workaround that lives in every query stops looking like a workaround and starts looking like how the data works. That's what makes it so hard to get prioritized later. Thanks for coming back to this thread, Om, I've enjoyed it.

Thread Thread
 
omyvnss profile image
Om Yaduvanshi •

that last step is the trap. once it reads as how the data works, fixing it looks like a breaking change, so nobody volunteers for it.

Collapse
 
posting-dude profile image
Posting Dude •

There's a small tension between "send outcomes from the server" and the LectureNotes activation being a student opening the first summary. Opening is a view, so it's exactly the kind of event ad blockers and flaky mobile clients eat. Do you record summary_viewed when the API serves the summary content, or does it come from the client? If it's client side, the activation rate is partly the ad-blocker rate you mention.

Collapse
 
nabeelbaghoor profile image
Nabeel Hassan •

Good catch, and you're right, that tension is real. "Opened the first summary" is a view, so if it only fires from the client, part of what you're measuring is ad blockers and dropped mobile requests. The way I'd resolve it in the contract: record activation on the server when the API serves a completed summary's content to that user for the first time, and keep a client-side summary_viewed (fired when the screen finishes rendering with content) as a secondary signal for screen-level questions. The gap between the two is useful on its own, since a big gap points at either instrumentation loss or people who fetched and bounced. The post should have spelled that out instead of leaving it as an example of a view event. Thanks for pushing on it.

Collapse
 
officialmailkr profile image
오피셜메일 •

LectureNotes의 활성화를 가입이나 생성 완료가 아니라 첫 요약 열람으로 정의한 예가 좋네요. summary_generated와 summary_viewed를 별개 사건으로 남기고, 같은 결과를 재시도해서 연 경우의 집계 기준까지 계약에 적으면 ‘생성은 됐지만 사용하지 않은 상태’를 놓치지 않겠습니다. 이벤트 타입뿐 아니라 발생 횟수와 순서도 시험하면 유용할 것 같습니다.

Collapse
 
nabeelbaghoor profile image
Nabeel Hassan •

Thank you! Yes, keeping summary_generated and summary_viewed as separate events is exactly how you catch "generated but never opened", and that gap is often the most useful number in the whole funnel. Good call on writing down how retries and re-opens of the same summary count, and on testing counts and order, not just event names. A contract that only checks names would happily pass a double-fired activation.