# Tracking plan — <product>

> Owner: <name>. This file is the source of truth for instrumentation. Version it
> with the code; ship breaking changes through a changelog entry, not a silent edit.
>
> Template by Goodlabs — https://goodlabs.kz · dilshat@goodlabs.kz
> Free to copy and adapt. Attribution appreciated, not required.

Fill this in **before** anyone opens a GTM container. Most broken analytics
setups are not a tagging problem — they are a missing-agreement problem: three
people each shipped the event they thought was meant.

---

## Naming conventions

- **Events:** `snake_case`, `object_action` — `payment_completed`, not
  `completedPayment` or `Payment Success`
- **Properties:** `snake_case`
- **Values:** lowercase and normalised. One canonical name per channel — pick
  `google` or `google_ads` and never both, or you will spend a quarter
  de-duplicating `google,google` in your channel report
- **Never:** PII in event properties, raw URLs as values, free-text user input

Write the convention down once here. A convention nobody can point at is a
preference, and preferences drift.

---

## Identity model

The single most expensive thing to retrofit. Decide it now.

| | Definition |
|---|---|
| **Anonymous id** | e.g. GA4 `client_id` / Amplitude `device_id`, first-party cookie, 400-day TTL |
| **User id** | e.g. internal `user_id`, hashed. **Assigned when:** login, registration, or first identified action — state which |
| **device → phone → user stitching** | Which key joins them, where the join runs (client, server, warehouse), and what happens to pre-login events |
| **Groups (account / company)** | B2B only: the account id, and whether metrics roll up to it |

Two questions worth answering in writing, because they surface every argument
early: what happens to a user's history when they log in on a second device,
and what happens when two accounts turn out to be the same human.

---

## Events

One row per event. If a row cannot be filled in, the event is not ready to build.

| Event | Trigger | Key properties | Destinations | Dedup key | Owner |
|---|---|---|---|---|---|
| `sign_up` | Registration confirmed server-side (not on form submit) | `method`, `plan` | GA4, Amplitude | `event_id` | |
| `checkout_started` | Checkout screen rendered, cart non-empty | `value`, `currency`, `items[]` | GA4, Meta CAPI | `event_id` | |
| `payment_completed` | Payment provider webhook returns success | `value`, `currency`, `payment_method`, `order_id` | GA4, Meta CAPI, TikTok, Amplitude | `order_id` | |
| `refund_issued` | Refund settled in the ledger | `value`, `currency`, `order_id`, `reason` | GA4, warehouse | `refund_id` | |
| | | | | | |

**Trigger** is the part teams skip, and it is the part that decides whether the
number is true. "On the thank-you page" and "when the payment webhook confirms"
produce different revenue, and only one of them survives a refund.

---

## Property dictionary

| Property | Type | Example | Event(s) | Note |
|---|---|---|---|---|
| `value` | number | `24990` | `checkout_started`, `payment_completed` | Minor units or major — pick one, say which |
| `currency` | string | `KZT` | any monetary event | ISO 4217, uppercase |
| `order_id` | string | `ord_10482` | `payment_completed`, `refund_issued` | Also the dedup key |
| `payment_method` | string | `card`, `apple_pay`, `installment` | `payment_completed` | Closed list — enumerate it here |
| `method` | string | `email`, `phone`, `apple` | `sign_up` | Closed list |
| | | | | |

Enumerate closed lists in this file. An open string property becomes 40 spellings
of the same value within two quarters.

---

## Destinations

| Destination | Details to fill in |
|---|---|
| **GA4** | Measurement ID, web or server-side container, which events are marked as key events |
| **Meta CAPI** | Dataset ID, which EMQ fields you send (email, phone, `fbp`, `fbc`, IP, UA), `event_id` for browser↔server dedup |
| **Google Ads** | Enhanced conversions: which identifier, hashed where |
| **TikTok Events API** | Pixel code, `event_id` dedup |
| **Amplitude / product analytics** | Project, `insert_id` for dedup, whether events arrive client-side or from the warehouse |
| **Warehouse** | Table, load cadence, whether it is the source of truth for revenue |

Note per event whether the browser or the server is authoritative. When both
fire, the dedup key is the only thing standing between you and double-counted
revenue.

---

## QA checklist

Run this before release, on staging, with the real destination in debug mode.

- [ ] Event fires exactly once — no duplicates on `router.replace()`, re-render, or back-navigation
- [ ] Properties are populated: no `undefined`, no `null` strings, no PII
- [ ] Dedup works: browser and server events with the same `event_id` / `insert_id` collapse into one
- [ ] Match quality (EMQ) checked in the destination's own UI, not assumed
- [ ] Consent Mode / gating respected — events suppressed when consent is denied, and consent state reaches the server container
- [ ] Currency and value verified against a real order in the ledger, not a test order
- [ ] Refunds and cancellations flow through — a plan that only tracks the happy path overstates revenue
- [ ] Verified on staging, not on production

---

## Changelog

| Date | Change | Breaking? | By |
|---|---|---|---|
| | | | |

---

## When this stops being a documentation problem

A tracking plan fixes disagreement. It does not fix a webview that drops the
session, a CAPI feed with 3/10 match quality, or offline revenue that never
comes back from the CRM.

Field notes on those: https://goodlabs.kz/wiki
Two-week audit, fixed scope: https://goodlabs.kz/#contact
