twelvebitDocsllms.txtSign in
View as Markdown

Tracking plan (analytics.yaml)

The tracking plan is a file in your repository: analytics.yaml. It lists every event your code sends, with a description and the props. The file is the source of truth. twelvebit keeps a copy, so the dashboard and the MCP server know your events, and so it can show events that are planned but never sent.

Full example#

yaml
version: 1
project: example.com
integrations: [stripe]
events:
  signup:
    description: A visitor creates an account
    tags: [activation]
    props:
      method:
        type: string
        enum: [email, google, github]
        description: How the account was created
  project_created:
    description: A user creates a project
    tags: [activation]
    props:
      template: { type: string, enum: [blank, blog, store] }
      from_onboarding: { type: boolean, required: false }
  checkout_started:
    description: A user opens the checkout
    tags: [revenue]
    props:
      plan: { type: string, enum: [starter, pro] }
      seats: { type: number, required: false }
  subscription_started:
    description: The first payment of a subscription succeeds (sent by the server)
    tags: [revenue]
    revenue: true
    props:
      plan: { type: string, enum: [starter, pro] }
dashboards:
  funnels:
    - name: Signup to paid
      steps: ["page:/pricing", signup, checkout_started, subscription_started]

Schema#

The schema is strict. An unknown key is an error, so a typo does not silently do nothing.

KeyTypeRules
versionnumberRequired. Must be 1.
projectstringYour domain, for example example.com.
integrationslistAny of stripe, polar, lemonsqueezy, paddle, dodo, shopify, other.
eventsmapRequired. Up to 200 events. Key = the event name.
events.<name>.descriptionstringRequired. Up to 500 characters.
events.<name>.tagslist of stringsUp to 10. Groups events in the dashboard.
events.<name>.revenuebooleantrue for events that carry money. Send them from the server.
events.<name>.propsmapUp to 30 props. Key = the prop name.
props.<name>.typestringRequired. string, number or boolean.
props.<name>.enumlist of stringsAllowed values. Only for type: string.
props.<name>.requiredbooleanProps are optional by default. true makes the prop required in the generated types.
props.<name>.descriptionstringUp to 500 characters.
dashboards.funnelslistUp to 20 funnels.
dashboards.funnels[].namestringRequired.
dashboards.funnels[].stepslist2 to 8 steps. A step is an event in this plan (signup or goal:signup), payment (twelvebit adds this goal for each payment), or a page (page:/pricing; * is a wildcard).

Names of events and props: lowercase snake_case, start with a letter (^[a-z][a-z0-9_]*$), at most 64 characters. Two names that give the same camelCase name in generated code (a_1 and a1, both a1) are rejected. The names pageview, identify, outbound, ping and payment are reserved by the script.

Typed helpers#

Generate them from the plan:

sh
npx -y https://twelvebit.app/cli/twelvebit-cli.tgz codegen

This writes analytics.gen.ts, one camelCase function per event on a track object, with the props as types, and then pushes the plan (use --no-push to skip that). Commit the file. The output looks like this (details can change between CLI versions):

ts
// analytics.gen.ts: generated by `twelvebit codegen` from analytics.yaml. Do not edit by hand.
type Value = string | number | boolean;
type Twelvebit = (name: string, props?: Record<string, Value>) => void;

// No-op on the server and when the script is blocked, so tracking never breaks the app.
function send(name: string, props?: object) {
  if (typeof window === "undefined") return;
  try {
    (window as unknown as { twelvebit?: Twelvebit }).twelvebit?.(name, props as Record<string, Value> | undefined);
  } catch {}
}

export type SignupProps = {
  /** How the account was created */
  method?: "email" | "google" | "github";
};

export const track = {
  /** A visitor creates an account */
  signup: (props?: SignupProps) => send("signup", props),
  // … one function per event: projectCreated, checkoutStarted, subscriptionStarted
};

export function identify(user: { user_id: string; email?: string; name?: string; [trait: string]: Value | undefined }) {
  send("identify", user);
}

If you do not use codegen, write a small helper by hand:

ts
// lib/track.ts
type Events = {
  signup: { method: "email" | "google" | "github" };
  project_created: { template: "blank" | "blog" | "store"; from_onboarding?: boolean };
  checkout_started: { plan: "starter" | "pro"; seats?: number };
};

declare global {
  interface Window {
    twelvebit?: (name: string, props?: Record<string, string | number | boolean | undefined>) => void;
  }
}

export function track<E extends keyof Events>(name: E, props: Events[E]) {
  if (typeof window !== "undefined") window.twelvebit?.(name, props);
}

// track("signup", { method: "google" });

Push the plan#

The CLI:

sh
npx -y https://twelvebit.app/cli/twelvebit-cli.tgz push

Or plain HTTP (YAML or JSON):

sh
curl -sS -X PUT https://twelvebit.app/api/v1/tracking-plan -H "Authorization: Bearer $TWELVEBIT_API_KEY" \
  -H "Content-Type: text/yaml" --data-binary @analytics.yaml

The response says what changed compared with the plan that twelvebit had: the events that were added, removed and changed, and whether the funnels changed. A plan that does not match the schema is rejected with status 422 (code invalid_plan) and a list of errors, each with a path (for example events.signup.props.plan.enum) and a message.

Add ?dry_run=1 to validate the plan and see the diff without saving it.

Read the stored plan with GET /api/v1/tracking-plan or npx -y https://twelvebit.app/cli/twelvebit-cli.tgz pull. It comes back as YAML in a fixed key order, without the comments of your file, so keep the file in git as the source of truth.

Drift checks#

Drift is when the code and the plan disagree. There are two checks.

In the code, without network access:

sh
npx -y https://twelvebit.app/cli/twelvebit-cli.tgz check --strict

It scans your source files for twelvebit("…") calls, track.…() calls of the generated helpers and data-tb-goal attributes, and compares them with analytics.yaml. It reports events that the code sends but the plan does not declare, events that the plan declares but the code never sends, and an analytics.gen.ts that is older than the plan. With --strict, it exits with code 1 when there is drift.

In production, the install check lists each planned event and whether twelvebit has received it:

sh
curl -sS https://twelvebit.app/api/v1/install-check -H "Authorization: Bearer $TWELVEBIT_API_KEY"

Run the check in CI#

yaml
# .github/workflows/analytics.yml
name: analytics
on: [pull_request]
jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npx -y https://twelvebit.app/cli/twelvebit-cli.tgz check --strict

To push the plan on merge to main, add a job that runs npx -y https://twelvebit.app/cli/twelvebit-cli.tgz push with TWELVEBIT_API_KEY from the repository secrets.