MainSDK
Search⌘K
How-to

Track product events without identifying people

Declare each product event and its props, send events with consent, and query active users, funnels and retention from the built-in store.

Building for 0.3

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

You want to know which features people use and where they drop off, without learning who anyone is. This guide declares two events, tracks them, and reads the numbers back. It takes about 20 minutes.

Built for 0.3 (spec analytics). The built-in store keeps events in the tenant's region; PostHog and an OpenTelemetry collector are optional drivers.

You need

  • A MainSDK app with privacy consent set up for the analytics purpose.

1. Declare the events

main.yaml
analytics:
  to: [builtin]
  without-consent: drop
  min-group: 10
  events:
    checkout.started:
      purpose: analytics
      props: { plan: "enum(free, pro)", items: int, coupon: "string?" }
    lesson.finished:
      purpose: analytics
      props: { subject: "enum(math, science, reading)", minutes: int }

Only declared events are accepted. A prop that would identify a person, such as email or ip, fails the check with MAIN-ANALYTICS-0011.

2. Track the events

app/checkout/actions.ts
"use server";
import { analytics, auth } from "mainsdk";

export async function startCheckout(req: Request, plan: string) {
  await auth.require(req);
  analytics.track("checkout.started", { plan, items: 3 });
}

The user and tenant come from the request. You never pass them in.

3. Read the numbers

scripts/funnel.ts
import { analytics } from "mainsdk";

const funnel = await analytics.query({
  kind: "funnel", steps: ["checkout.started", "lesson.finished"], within: "7d",
  from: "2026-10-01", to: "2026-10-31",
});

Rows for fewer than min-group people are hidden, and the result says how many were hidden.

From the terminal, mainsdk analytics asks the same questions across every tenant:

text
npx mainsdk analytics --query funnel --steps checkout.started,lesson.finished --within 7d

Check it worked

  • mainsdk check passes with no MAIN-ANALYTICS-0010 or 0011 findings.
  • Tracking an event you did not declare throws MAIN-ANALYTICS-0001 in development.
  • Without consent for the purpose, the event is dropped and no row is stored.

If it fails

  • MAIN-ANALYTICS-0002: props do not match the declared types. Send values that match.
  • MAIN-ANALYTICS-0004: a query names an undeclared event or uses a by its kind does not take.
  • MAIN-ANALYTICS-0007: analytics.query needs builtin in analytics.to.

Prompt for your coding agent

Track checkout.started and lesson.finished with MainSDK's analytics interface. Declare their props with types, keep identifying fields out, and add a funnel query that a non-technical admin can read in the app.

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet