# Tracking Events

> Reference and investigation playbook for analytics tracking in sites. Use when locating where an event is fired, understanding the deferred-analytics provider, adding/changing a Segment event, debugging missing events in the warehouse, or reasoning about download/onboarding/Click funnels. Triggers on "Segment", "analytics event", "tracking", "useTrackClick", "useDeferredTrack", "useAnalytics", "data-event", "download_started", "download_success", "download_failed", "Onboarding Checkpoint", "REELS_*", "GO_TO_EXPLORER", "page tracking", "where is X fired", "what tracks X", "anon_user_id", "dónde se manda evento", "utm", "campaign attribution", "partner attribution", "download_target".

- Skill: `decentraland/tracking-events` (Agent Skill)
- Install (CLI): `npx skillmds@latest add decentraland/tracking-events`
- Raw SKILL.md: https://api.skillmd.com/api/skills/decentraland/tracking-events/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Marketing & Growth
- Author: decentraland (https://skillmd.com/u/decentraland)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/decentraland/tracking-events

---


# tracking-events

The single source of truth for understanding analytics in sites. **Read top-to-bottom the first time** so the conventions stick; then use the section links as a reference.

## 1. The stack

- **Provider:** Segment (analytics-next). Initialized lazily via `DeferredAnalyticsProvider` (`src/modules/DeferredAnalyticsProvider.tsx`), which schedules the writeKey load via `requestIdleCallback` with a 4s fallback timer. That means **on cold loads, `useAnalytics().isInitialized` is `false` for up to ~4s**.
- **Underlying lib:** `@dcl/hooks` (workspace submodule `hooks/`) exposes `useAnalytics()` which returns `{ isInitialized, track, identify, page }`. When `isInitialized === false`, `track` is a no-op — calls just drop. There is **no built-in queueing** in `useAnalytics`.
- **Contentsquare:** activated alongside Segment via `scheduleDeferredThirdParty` in `src/modules/deferredThirdParty.ts`. Out of scope here; it's session recording, not event tracking.
- **Anonymous id:** `@segment/analytics-next` stores its anonymous id in `localStorage` as `ajs_anonymous_id` (JSON-encoded). Read it via `useAnonUserId()` (`src/hooks/useAnonUserId.ts`) which validates against UUID format and also accepts an `?anon_user_id=…` URL param override (used by the download success → launcher → Explorer attribution chain).
- **First-party proxy:** ad-blocker filter lists match `cdn.segment.com` and `api.segment.io`, so those sessions send nothing. Two env keys route Segment through Decentraland's own domain (`src/modules/segmentConfig.ts`): `SEGMENT_CDN_URL` (`https://evs.e.decentraland.org`, a CloudFront distribution over static objects: settings + the GA4 remote plugin, passed to the SDK as `cdnUrl`) and `SEGMENT_API_HOST` (`api.e.decentraland.org/v1`, the proxied Tracking API: SDK delivery plus the base of the beacon's track URL via `getSegmentTrackUrl()`). They are **different hosts** and not interchangeable, the CDN one 404s on the ingestion paths. Both are validated: a value that is not an absolute https URL is dropped with a warning and Segment's own hosts are kept. **Gotcha:** the proxy is only first-party on `decentraland.org`. On `.zone` / `.today` it is third-party, so EasyPrivacy's blanket `$ping,third-party` rule still drops `navigator.sendBeacon` there and `postSegmentEvent` degrades to its `fetch keepalive` fallback.

## 2. Two abstraction layers — infra and domain

Tracking in sites is split into two layers that compose cleanly:

- **Infra (`useDeferredTrack`)** — wraps `useAnalytics().track` with a queue. Doesn't know what the event is or what the payload means. Its only job: make sure the track call survives Segment's lazy boot. Adds `track_called_at`, `track_delivered_at`, `track_deferred` to every payload so the data team can audit deferral. **Caveat:** the queue is component-scoped — pending events are dropped on unmount. Fine for events on routes where abrupt departure is unlikely; wrong choice for events on routes users are likely to leave abruptly (see `postSegmentEvent` below and LL-10).
- **Infra (`postSegmentEvent` + `ensureSegmentAnonymousId`)** — the unload-safe alternative (`src/modules/segmentBeacon.ts` + `src/modules/segmentAnonymousId.ts`). Posts directly to Segment's HTTP Tracking API via `navigator.sendBeacon` (falling back to `fetch keepalive`), bypassing `useAnalytics()`/`useDeferredTrack()` entirely — so it works even if Segment hasn't booted yet, and survives a same-tick page unload. `ensureSegmentAnonymousId()` mints/persists a Segment-adoptable anonymous id (the same localStorage shape analytics-next writes) so events fired before Segment boots still carry a real, adoptable id instead of a throwaway one. Because it hand-builds the payload (analytics-next isn't in the loop), it also resolves the identified `userId` itself via `resolveSegmentUserId()` (`src/modules/segmentUserId.ts`, reads the SDK's own `ajs_user_id` key) and attaches the SDK-parity context (`integrations: {}`, `context.page.search`, `context.timezone`, `context.userAgentData`). It deliberately omits `type` (the `/v1/track` endpoint infers it) and `_metadata` (SDK-internal device-mode routing — adding it risks double-counting).
- **Domain (`createDownloadTracker`, future per-funnel factories)** — knows the schema of a specific funnel. Builds the canonical payload, captures timestamps tied to domain semantics (`started_at`, `succeeded_at`), exposes a small intent-shaped API (`.started()` / `.success()` / `.failed()`). Doesn't know about Segment loading — internally fires via whichever transport its factory chose (`useDeferredTrack` or `postSegmentEvent`).

You should almost never call `useAnalytics().track` directly. The decision tree:

- Click handler driven by `data-*`? → `useTrackClick` (which already uses `useDeferredTrack` internally).
- Download funnel event (`download_started/_success/_failed`)? → `createDownloadTracker(ctx)` — fires via `postSegmentEvent` (see 5.3).
- New domain that needs queueing AND fires on a route users don't abruptly leave? → call `useDeferredTrack()` and pass it to whatever domain factory you build.
- New domain that fires on a route users are likely to abruptly leave (installer pages, exit-intent diagnostics, etc.)? → use `postSegmentEvent` + `ensureSegmentAnonymousId()` directly, following `downloadTracking.ts` / `downloadFunnelExit.ts`. Don't reach for `useAnalytics().track` directly unless you accept silent-drop on cold loads.

## 3a. The hooks — pick the right one

| Hook                                                | Where it lives                                                       | When to use                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| --------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `useAnalytics()`                                    | `@dcl/hooks`                                                         | When you need the raw Segment primitives. Tracks fired before Segment loads will silently drop. Rare; prefer the wrappers below.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `useDeferredTrack()`                                | `src/hooks/useDeferredTrack.ts`                                      | When the event must survive Segment's lazy init. Returns a function with the same signature as `track`, but if `isInitialized === false` it queues the call and drains the queue when Segment becomes ready. Preferred default for any track fired from a component that mounts on a route the user can deep-link to — **except** events on routes users are likely to abruptly leave (installer pages, exit-intent diagnostics): the queue is component-scoped and drops pending events on unmount. `download_started/_success/_failed` used to be the textbook example of this hook's intended use; it no longer is — see 5.3 and LL-10. |
| `postSegmentEvent()` + `ensureSegmentAnonymousId()` | `src/modules/segmentBeacon.ts` + `src/modules/segmentAnonymousId.ts` | Unload-safe alternative to `useDeferredTrack` for events fired on routes users are likely to abruptly leave. Fires immediately via `navigator.sendBeacon`/`fetch keepalive`, independent of Segment's own boot state. Used by `useDownloadClick`, `downloadFunnelExit.ts`, and `createDownloadTracker` (download_started/\_success/\_failed).                                                                                                                                                                                                                                                                                              |
| `useTrackClick()`                                   | `src/hooks/adapters/useTrackLinkContext.ts`                          | Click handlers driven by `data-*` attributes on the clicked element. **Always emits `SegmentEvent.CLICK`** as the event name; the action subtype lives in the payload as `event` (sourced from `data-event`). Strips `payload.event` when it would equal the event name (`Click`). Uses `useDeferredTrack` internally — no silent drops.                                                                                                                                                                                                                                                                                                   |
| `useBlogPageTracking()`                             | `src/hooks/useBlogPageTracking.ts`                                   | Per-route `page()` event for Helmet-titled routes where the automatic `page()` in `Layout.tsx` races the async title write. See sites CLAUDE.md rule 23.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `useLegacyRedirectTracking()`                       | `src/hooks/useLegacyRedirectTracking.ts`                             | Specialized: waits up to 800ms for Segment to load, then emits one of `LEGACY_EVENTS_REDIRECTED` / `LEGACY_PLACES_REDIRECTED` and proceeds with the `<Navigate>`. Pattern reference for "block briefly on analytics readiness then unblock UI".                                                                                                                                                                                                                                                                                                                                                                                            |

## 3b. The event enums — where to find names

All declared in `src/modules/segment.types.ts` and re-exported by `src/modules/segment.ts`:

- **`SegmentEvent`** — every event name fired with `track()`. Mixed casing because some literals are historical: `'Click'`, `'Download'`, `'Reels Click …'` (Title Case Words), and the funnel events `download_started / download_success / download_failed` (snake_case). Don't normalize — the data team tracks by these literal strings.
- **`DownloadPlace`** — kebab-case enum for the `place` field of `download_*` events. 16 values incl. `landing-hero`, `landing-hero-epic`, `landing-hero-platform-switch`, `come-hang-out`, `come-hang-out-platform-switch`, `jump-in-already-user`, `play-hero`, `play-hero-epic`, `play-hero-app-store`, `play-hero-google-play`, `play-experimental-web`, `download-page`, `download-success-footer`, `creator-hub-download-page`, `creator-hub-success-page`, `unknown` — see `src/modules/segment.types.ts` for the authoritative list (it grows; don't copy this one stale).
- **`SectionViewedTrack`** — Title Case enum for the `place` field of `Click` events (consumed via `data-place`). Values: `Landing Hero`, `Creators Hero`, `Landing Explore`, etc. **Different namespace from `DownloadPlace`** — they happen to overlap in intent (e.g. `SectionViewedTrack.LANDING_HERO = 'Landing Hero'` vs `DownloadPlace.LANDING_HERO = 'landing-hero'`), but to join them in a query the warehouse has to normalize.

## 4. The `data-event` convention (Click events)

`useTrackClick()` reads ALL `data-*` attributes on the clicked element, converts each to camelCase, and merges them into the payload. The hook **always emits `SegmentEvent.CLICK`** as the event name — the action subtype lives in `payload.event`.

**Rules for callers:**

- `data-event` MUST be a `SegmentEvent` enum value, never a hardcoded literal. The lowercase `"click"` literal is no longer accepted as a special value — use `data-event={SegmentEvent.CLICK}` if you want to be explicit.
- If `data-event` equals `SegmentEvent.CLICK`, the adapter strips it from the payload (would otherwise duplicate the event name).
- `data-place` is the canonical key for "where the click happened" — use `SectionViewedTrack.X` enum values.

**Examples in current code:**

- `data-event={SegmentEvent.DOWNLOAD}` → Hero Download / Epic / Create/Hero (primary + secondaries) / CreatorHubDownload (primary + secondaries) / DownloadSuccess footer. Payload includes `event: 'Download'`.
- `data-event={SegmentEvent.REELS_CLICK_DCL_LOGO}` → Reels Logo. Payload includes `event: 'Reels Click Decentraland Logo'`.
- `data-event={SegmentEvent.CLICK}` → WhatsOn / WhatsOnCard / WeeklyRituals / ComeHangOut / CatchTheVibe / JumpIn (via CTAButton default). Payload does NOT include `event` (stripped because it would duplicate the event name).

**Common other data-\* attrs that flow into payloads:** `data-section`, `data-card`, `data-os`, `data-title`, `data-subtitle`.

## 5. The download funnel (Explorer)

Three event families live across the funnel:

### 5.1 Click upstream (any CTA decorated with `data-event={SegmentEvent.DOWNLOAD}`)

Fired by `useTrackClick()`. Payload: `{ place: '<Section Title>', anything-else-from-data-* }`.

### 5.2 ~~Onboarding Checkpoint~~ — DEPRECATED 2026-05-22

The `'Onboarding Checkpoint'` family (CP5 reached / completed, CP6 reached) was **fully removed** from the codebase. The helper at `src/modules/onboardingCheckpoint.ts` is gone; `DownloadLayout` and `DownloadOptions` no longer fire it. If a future requirement needs a download-funnel checkpoint, design from scratch rather than reviving this — the original schema (numeric `checkpointId`, `email`/`wallet` PII in payload) was the reason it got cut.

### 5.3 `download_started / _success / _failed`

Fired by `src/pages/DownloadSuccess/DownloadSuccess.tsx` via `createDownloadTracker` (`src/modules/downloadTracking.ts`).

**Two flows in the same component:**

- **Auto-flow** (page mount `useEffect`): the user landed on `/download_success?os=…&arch=…&place=…&anon_user_id=…` after a redirect from Hero / DownloadOptions / ComeHangOut / JumpIn. The download is triggered automatically.
- **Footer re-download** (`handleDownloadClick`): user clicks the "download again" link in the success page footer. Same shape but `place = 'download-success-footer'` is hardcoded.

**Payload shape (both flows, see `src/modules/downloadTracking.ts`):**

```ts
DOWNLOAD_STARTED → {
  place?,                              // omitted if UNKNOWN
  href,                                // the actual downloadUrl streamed (gateway + anon_user_id)
  os, arch,
  anon_user_id?,                       // omitted if undefined
  auth_state,                          // 'authenticated' | 'anonymous'
  revisit,                             // 0 = first visit, n = n-th revisit (sessionStorage counter per os:arch)
  started_at                           // ms timestamp at .started() call time
}

DOWNLOAD_SUCCESS → DOWNLOAD_STARTED's payload + {
  filename,
  succeeded_at, duration_ms,
  bytes_transferred?                   // only on Windows streamed path (downloadFileWithProgress)
}

DOWNLOAD_FAILED → DOWNLOAD_STARTED's payload + {
  reason,                              // error.message
  failed_at, duration_ms
}
```

**Timing semantics:**

- `_STARTED` fires **before** `streamOrFallback` (intent signal). If user closes the tab during the stream → `_STARTED` is in the warehouse, no `_SUCCESS` / `_FAILED`. Pair-wise diff `count(_STARTED) - count(_SUCCESS) - count(_FAILED)` ≈ abandon rate.
- `_SUCCESS` fires **after** `streamOrFallback` resolves. Note: this is "bytes arrived at the browser" (Windows) or "estimated time elapsed" (macOS) — NOT "user clicked Save". There is no clean signal for actual disk-write without `showSaveFilePicker`, which is Chromium-only and breaks Safari/Firefox and the macOS `kMDItemWhereFroms` attribution. The `os` field in the payload tells consumers which semantic applies.
- `_FAILED` fires in the `.catch()` branch. If the failure is in `calculateDownloadUrl` (i.e. before the tracker was built), a fallback tracker is constructed in the catch with `href = osLink` (CDN fallback URL) and the same shape is preserved.

**Revisits:** the previous `sessionStorage` / `history.state` idempotency bails were removed. Every aterrizaje en `/download_success` re-runs the full flow and emits events with `revisit: n`. The counter is keyed by `os:arch` combo (`sessionStorage:downloadSuccess:visits:${os}:${arch}`).

**Transport:** events fire via `postSegmentEvent()` (`src/modules/segmentBeacon.ts`) with `ensureSegmentAnonymousId()` (`src/modules/segmentAnonymousId.ts`) as the anonymous id — NOT through `useDeferredTrack()`. This changed because `/download_success` is the page users are most likely to abruptly leave (they're about to run the installer they just downloaded), and `useDeferredTrack`'s queue is component-scoped: any event still queued when the component unmounts is silently dropped. Since Segment's lazy boot (`DeferredAnalyticsProvider`, up to ~4s idle timeout) is frequently slower than the flow that fires `tracker.started()`, a meaningful fraction of these events were being queued and then lost on navigation. `postSegmentEvent` posts directly via `navigator.sendBeacon` (falling back to `fetch keepalive`), independent of Segment's own init state, and `ensureSegmentAnonymousId()` mints/persists a real Segment-adoptable id so attribution survives even when the event fires before Segment ever boots. See `useDownloadClick` (PR #636) and `downloadFunnelExit.ts` (PR #632) for the precedent this migration followed, and LL-10 below. **Timestamps are still captured at call time** (`started_at` / `succeeded_at` / `failed_at`), so timing analysis is unaffected by the transport change.

### 5.4 `Click` upstream — where the data lands

When the upstream `Click` (post-P0-1 fix: `'Download'` event name) is correctly fired with `place: 'Landing Hero'` (Title Case `SectionViewedTrack`), but `download_started` arrives with `place: 'landing-hero'` (kebab-case `DownloadPlace`). They're the same intent — different namespace. The data team must normalize to join them.

### 5.5 Partner (UTM) attribution + `download_target` — PR #654, 2026-07-02

Marketing shares links like `https://decentraland.org/download?utm_source=shefi&utm_campaign=…`. Two things had to be threaded through the whole funnel: the UTM params themselves, and a `download_target` dimension (`desktop_installer` / `app_store` / `google_play`) so the warehouse can split desktop installer activations from mobile store exits (the latter never reach `/download_success`).

**Campaign params — `src/modules/campaignParams.ts` (new):**

- `CAMPAIGN_PARAM_KEYS` — allowlist of the 5 params: `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`.
- `collectCampaignParams(source?: URLSearchParams)` — reads those keys off `source` (defaults to `window.location.search`, SSR-guarded), truncates each value to `MAX_CAMPAIGN_VALUE_LENGTH = 256` chars, omits absent/empty ones.
- `withCampaignParams(path)` — appends the currently-collected params to a path. Used for the `/download` fallback href rendered before `userAgentData` resolves (Hero, ComeHangOut, PlayPage).
- **Documented limitation (in the file's own docstring):** params are read from the CURRENT URL at call time, not persisted per session. A visitor landing on `/?utm_source=…` who then browses internally before clicking a download CTA loses the attribution — the query string is gone by the time `collectCampaignParams()` runs. Partner links must point directly at a page hosting download CTAs (`/`, `/download`, `/play`).
- Kept snake_case (`utm_source`, not `utmSource`) so the keys match both the raw partner-sent param names and the Segment payload convention (LL-3) — no renaming needed to flow into tracking payloads.

**`src/hooks/useDownloadSuccessHref.ts` (new):** returns a memoized `(os, place) => href` builder for `/download_success` links: `buildDownloadSuccessHref(os, place, { anonUserId, campaignParams: collectCampaignParams() })`. `anonUserId` is captured via `useAnonUserId()` at hook scope, but `collectCampaignParams()` is called **inside the returned callback**, not at hook render time — i.e. re-evaluated fresh on every click, not cached. Intentional: it keeps the params live for the button's actual click moment rather than whatever the URL was when the component last rendered. Replaces a local `useCallback(() => buildDownloadSuccessHref(os, place, { anonUserId }), [anonUserId])` that used to be duplicated in Hero, ComeHangOut, and PlayPage — now they all call this hook (`DownloadOptions` still composes `buildDownloadSuccessHref` inline because it also needs to pass `arch`).

**`src/modules/segment.types.ts` — `DownloadTarget` enum (new):**

```ts
enum DownloadTarget {
  DESKTOP_INSTALLER = 'desktop_installer',
  APP_STORE = 'app_store',
  GOOGLE_PLAY = 'google_play',
  EPIC = 'epic',
  CREATOR_HUB = 'creator_hub'
}
```

Set via `data-download-target={DownloadTarget.X}` on every download CTA (Hero, ComeHangOut, DownloadOptions, DownloadLayout's mobile store badges, PlayPage, Creator Hub CTAs) and read into payloads by `useDownloadClick`, `useTrackClick`, and `buildTrackerExtra` (below).

**`src/modules/url.ts` — `DownloadSuccessHrefOptions.campaignParams`:** the param-assembly loop in `buildDownloadSuccessHref` guards against a campaign key clobbering a routing param: `if (params.has(key)) continue` before `params.set(key, value)`, so no `utm_*` can overwrite `os`/`place`/`arch`/`anon_user_id`. Unreachable today (the `utm_*` allowlist in `collectCampaignParams` can't collide with those names), but the option accepts a bare `Record<string, string>` so a future caller passing raw `searchParams` entries can't corrupt the funnel.

**`src/hooks/useDownloadClick.ts` — merge order:** the returned click handler builds `payload = { ...collectCampaignParams(), ...dataAttributes }` — `data-*` attributes are spread **after** campaign params, so a same-named `data-*` attribute wins on collision (campaign params never override component-controlled data). `downloadTarget` is destructured out of `dataAttributes` first and re-added as `payload.download_target` (`readDataAttributes` camelCases `data-download-target` → `downloadTarget`; the warehouse dimension is snake_case) — this rename happens once on the shared payload object before the warm/cold branch, so both transports (`deferredTrack` when Segment is warm, `postSegmentEvent` when cold) get the same snake_case key.

**`mac_arch` (`apple_silicon` | `intel` | `unknown`) — 2026-07-21:** Mac-only GPU-based architecture hint attached inside the shared `buildClickPayload` (`src/hooks/adapters/clickPayload.helpers.ts`, consumed by both `useDownloadClick` and `useTrackClick`) to any Click that carries a `download_target` (omitted entirely off-macOS). Source: `src/modules/macArchHint.ts` — reads the WebGL unmasked renderer once per page load (memoized), because the UA reports "Intel Mac OS X" even on Apple Silicon and `userAgentData.architecture` comes back empty. Why it exists: the launcher DMG is arm64-only; an Intel Mac downloads it, cannot open it, and fires zero launcher telemetry — this property is the only place in the pipeline where that doomed cohort is measurable. The macOS gate matches "Macintosh" only — iPhone/iPad UAs say "like Mac OS X" but never "Macintosh", so iOS store-badge taps stay excluded — and iPads in desktop mode (Macintosh UA but `maxTouchPoints > 1`; real Macs report 0) are excluded too, so they can't dilute the Intel share. NVIDIA/GeForce/Quadro renderers classify as `intel` (NVIDIA GPUs only ever shipped in Intel-era Macs).

**`src/pages/DownloadSuccess/DownloadSuccess.tsx` — `buildTrackerExtra()`:** builds the shared `extra` object merged into every `download_started/_success/_failed` payload (`downloadTracking.ts`'s `buildBasePayload` spreads `ctx.extra` first, so core schema fields still win on collision):

```ts
{
  ...(collectClientFingerprint() ?? {}),
  ...campaignParamsRef.current,
  ...(correlation ? { click_id: correlation.click_id, ms_since_click: Date.now() - correlation.clicked_at } : {}),
  download_target: DownloadTarget.DESKTOP_INSTALLER
}
```

Only `collectClientFingerprint()` is wrapped in try/catch. Campaign params, click correlation, and `download_target` are spread outside that catch so a fingerprint failure cannot drop attribution (P2-4 resolved).

**`src/components/Layout/DownloadLayout.tsx`:** the mobile store badges (Google Play / App Store) call `useDownloadClick()` (aliased `trackStoreExit`) with their own `data-*` attributes (`data-download-target={DownloadTarget.GOOGLE_PLAY | APP_STORE}`, `data-os`, `data-place={DownloadPlace.DOWNLOAD_PAGE}`). This is the only attribution signal for these exits — they leave to the store and never reach `/download_success`, so there's no `download_started` for them; the beacon-backed `Click` event is the whole record.

**Hero.tsx / ComeHangOut.tsx:** both replaced their local `downloadSuccessHref` builder with `useDownloadSuccessHref()` and added `data-download-target` to every download CTA (desktop button, Epic button, platform-switch icons, mobile store buttons). `PlayPage.tsx` and `DownloadOptions.tsx` got the same `data-download-target` additions (all `DESKTOP_INSTALLER` except the store badges).

### 5.6 Click correlation + funnel diagnostics — PR #675, 2026-07-07

This PR instruments the blind window between the upstream `Click` and `download_started` without changing existing funnel event semantics.

**`click_id` / `clicked_at`:** `src/modules/downloadClickCorrelation.ts` mints `{ click_id, clicked_at }` with `generateUuid()` and stores it in `sessionStorage` under `downloadFunnel:lastClick`. `readDownloadClickCorrelation()` returns only fresh, valid records; max age is 30 minutes. `useDownloadClick()` attaches the same object to the upstream `Click`, and `/download_success` reads it back so `download_started/_success/_failed` can include `click_id` plus `ms_since_click`.

**`download_success_arrived`:** `DownloadSuccess.tsx` fires this immediately on mount via `postSegmentEvent` + `ensureSegmentAnonymousId()`, before the download attempt starts. It includes `os`, `arch`, `revisit`, `auth_state`, UTM params, `download_target=desktop_installer`, optional `click_id`/`ms_since_click`, and always includes `place` (including `unknown`) so direct or malformed landings are measurable.

**`download_page_exit`:** `/download` mounts `useDownloadPageExit()` from `DownloadLayout.tsx`. The hook resets a module-level CTA flag on mount, then sends `download_page_exit` on every `visibilitychange -> hidden` with `cta_clicked`, `ms_on_page`, and campaign params. `useDownloadClick()` marks the CTA flag on every download CTA click. There is intentionally no fire-once guard; the warehouse can collapse multiple rows.

**`download_redirect_failed`:** `DownloadOptions.tsx` wraps only `getDownloadLinkWithIdentity()` in a try/catch when `downloadOnClick` is enabled. On failure it emits `download_redirect_failed` with `{ os, arch?, place: 'download-page', reason, download_target: 'desktop_installer', utm_*? }` and rethrows, preserving the previous behavior where a dispatch failure aborts the redirect.

**`download_target` coverage:** `useTrackClick()` now mirrors `useDownloadClick()` by renaming `data-download-target` / `downloadTarget` into payload key `download_target`. Creator Hub CTAs use `DownloadTarget.CREATOR_HUB` on their existing `Click` events only; do not add a `creator_hub_download_*` event family. `DownloadTarget.EPIC` separates Epic Store exits from desktop installer activations. The proposed ComeHangOut `Click` -> `Download` change was deliberately not executed; changing existing funnel buckets is a data-team decision, not part of this tracking-only PR.

## 6. The Creator Hub funnel — current state

**Upstream `Click` + `download_started` + `download_success`, reusing the shared enum — NOT a separate `creator_hub_download_*` family.** Shipped by PR #619 (merged 2026-06-23, predates the 2026-07 tracking work in this doc).

- **Click** — the primary download CTAs (CreatorsHero, `/download/creator-hub` page) emit `Click` with `place=Creators Hero` / `place=Download` and `event=Download` via the standard `useTrackClick` adapter, same as any other download surface.
- **`download_started`** — fired from `src/hooks/useCreatorHubDownload.ts` (`handleDownload`) via `createDownloadTracker(...).started()` at the moment the file download is triggered (`place: DownloadPlace.CREATOR_HUB_DOWNLOAD_PAGE`). `revisit` is hardcoded `0` — a click is a one-shot intent, there's no per-attempt revisit notion on this page.
- **`download_success`** — fired from `src/pages/download/CreatorHubDownloadSuccess.tsx` on mount (`useEffect`, guarded by a ref so React strict-mode double-invoke only fires once) via `createDownloadTracker(...).success(filename)`, `place: DownloadPlace.CREATOR_HUB_SUCCESS_PAGE`. **Semantically this is "the visitor reached the post-download page", not "bytes arrived"** — sites can't observe the actual download outcome for this flow (see below), so reaching the success page is used as the completion signal. `revisit` increments per mount for the same `os:arch` (sessionStorage counter, mirrors the Explorer `DownloadSuccess` pattern).
- **No `download_failed`** — still true, and still by design: the Creator Hub download is `dispatch-and-forget` (`triggerFileDownload` + a 3s `setTimeout` redirect, no stream to observe). There is no browser-observable failure signal to fire it from.
- The footer re-download on `/download/creator-hub-success` fires `Click` via the standard `useTrackClick` adapter with `data-place={SectionViewedTrack.CREATOR_HUB_SUCCESS_FOOTER}`, `data-os`, `data-download-target={DownloadTarget.CREATOR_HUB}` so analytics can distinguish footer clicks from primary CTAs.
- No `page()` event on the success page.

## 7. Adjacent / route-level tracking

- **Automatic `page(pathname)`** in `src/components/Layout/Layout.tsx` (~line 16), runs on every route change unless `isPageTrackingExempt(pathname)` (`src/components/Layout/Layout.helpers.ts`) exempts it.
- **Exempt paths:** `/brand`, `/content`, `/download`, `/ethics`, `/privacy`, `/referral-terms`, `/rewards-terms`, `/security`, `/terms`. These skip the automatic page() — but **manual track() calls fire as usual**. The comment on the constant has been historically misleading; clarify if you touch it.
- **Routes with NO `page()` at all:** `/download_success`, `/download/creator-hub`, `/download/creator-hub-success`, `/reels/*`, `/invite/:referrer`. These are Layout-less and don't get the automatic page(). Some (e.g. `/blog/*`) use `useBlogPageTracking` to fire their own page() event.

## 8. Other event domains — pointers

| Domain           | Where the fires live                                                                                                                                         |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Reels actions    | `src/components/Reels/ImageActions/ImageActions.tsx`, `Metadata.tsx`, `UserMetadata.tsx`, `WearableMetadata.tsx`, `ReelsListPage.tsx`, `Reels/Logo/Logo.tsx` |
| Communities      | `src/components/social/CommunityDetail/CommunityInfo/CommunityInfo.tsx` (`COMMUNITY_CLICK_*` family)                                                         |
| Report Player    | `src/components/Report/ReportForm/ReportForm.tsx` (`REPORT_PLAYER_*` family)                                                                                 |
| Storage          | via `src/hooks/useStorageTrack.ts` (`STORAGE_*` families, injects `realmName`/`parcel`/`address`)                                                            |
| Jump             | `src/hooks/useLaunchExplorer.ts` fires `GO_TO_EXPLORER` (shared by `src/components/jump/JumpInButton/` and `EditProfileButton`)                              |
| Legacy redirects | `src/hooks/useLegacyRedirectTracking.ts` (`LEGACY_EVENTS_REDIRECTED`, `LEGACY_PLACES_REDIRECTED`)                                                            |
| Invite Hero      | `src/components/Invite/InviteHero/InviteHero.tsx` — migrated to `useTrackClick` + `data-place` (see LL-3 below).                                             |

## 9. How to find where event X is fired

```bash
# 1. Find the enum entry to get the literal string value.
rg -n "SegmentEvent\\.X|'X event literal'" src/modules/segment.types.ts

# 2. Grep all track() calls for it.
rg -n "track\\(\\s*SegmentEvent\\.X|track\\(\\s*'X event literal'" src/

# 3. If it's a Click subtype, also search data-event:
rg -n 'data-event=\\{SegmentEvent\\.X\\}|data-event="X literal"' src/

# 4. For Storage / Reels families that pass the enum through a wrapper:
rg -n "useStorageTrack\\(\\)" src/
```

If grep returns zero matches the enum value is **dead code** — verify with a repo-wide grep before assuming an event still fires.

## 10. How to add a new tracking event

1. **Add the literal** to the appropriate enum in `src/modules/segment.types.ts`. Match existing casing within its family (snake_case for funnel events, Title Case for "section" events, kebab-case for `DownloadPlace`-like dimensions).
2. **Decide the transport:** is the event fired from a click-handled DOM element? `useTrackClick` via `data-event`. From inside a component lifecycle on a route the user can deep-link to but is unlikely to abruptly leave? `useDeferredTrack`. From inside a component lifecycle on a route users ARE likely to abruptly leave (installer pages, exit-intent diagnostics)? `postSegmentEvent` + `ensureSegmentAnonymousId()` — see 5.3 and LL-10. From a context where Segment is guaranteed ready (e.g. inside a `useEffect` that already awaits something Segment-y)? `useAnalytics` is fine.
3. **Payload conventions:**
   - snake_case keys (the codebase has eslint-disable comments for `auth_state`, `anon_user_id`, etc. — keep the existing pattern).
   - Capture client timestamps for any event whose timing matters; assume Segment ingestion delay is non-zero.
   - Omit optional fields when they'd be `undefined` rather than sending null — keeps warehouse rows cleaner.
4. **Tests:** add a unit test that asserts both the event name and the payload shape. See `src/modules/downloadTracking.spec.ts` for a per-event matcher pattern and `src/pages/DownloadSuccess/DownloadSuccess.spec.tsx` for an integration shape with mocked hooks.
5. **Coordinate with data team if the event is consumed by an existing dashboard.** Don't rename existing events — the warehouse joins on the literal name. Adding new fields is safe; removing/renaming requires a parallel-emission window.

## 11. Outstanding / known issues

- **P0-1** (✅ shipped): `useTrackClick` ignoring `data-event` for non-Click events — fixed; verify with the dead-enum grep above when touching callsites.
- **P0-2** (✅ done — Onboarding Checkpoint family deprecated 2026-05-22): all CP5/CP6 fires and the `trackCheckpoint` helper were removed. No replacement scheduled.
- **P0-3** (✅ done — PR #619): Creator Hub now fires `download_started` (on click) and `download_success` (on success-page mount) via the shared `createDownloadTracker`, reusing `SegmentEvent.DOWNLOAD_STARTED/_SUCCESS` — NOT a separate `CREATOR_HUB_DOWNLOAD_*` enum family. No `_FAILED` (still no observable failure signal). See section 6.
- **P1-1** (✅ done): `download_started/success/failed` payload + timing fixes. See Plan.md section.
- **P1-2** (✅ done — `useAnonUserId` reactivity 2026-05-22): hook now depends on `isInitialized` so it re-evaluates when Segment boots; `DownloadSuccess` gates the auto-download on a `anonUserIdReady` state with an 800ms timeout fallback. See LL-9.
- **P1-3** (✅ partial via `useDeferredTrack`): `useTrackClick` silent drop when `isInitialized === false`. Adopting `useDeferredTrack` inside the adapter would resolve this for Click events too.
- **P1-4** (✅ done — `download_started/_success/_failed` drop-on-unmount fixed 2026-07-01): these events used to fire via `useDeferredTrack`, whose queue is component-scoped and drops pending events on unmount — a real risk on `/download_success`, the page users are most likely to abruptly leave. `createDownloadTracker` now fires via `postSegmentEvent` + `ensureSegmentAnonymousId()` instead, matching the `useDownloadClick` (PR #636) / `downloadFunnelExit.ts` (PR #632) precedent. See 5.3 and LL-10.
- **P2-4** (✅ done — PR #668): `DownloadSuccess.tsx` now catches only `collectClientFingerprint()` inside `buildTrackerExtra()`. Campaign params, click correlation, and `download_target` are always spread outside that catch, so a fingerprint failure no longer drops attribution.
- **P2 list:** see Plan.md.

## 12. Lessons learned — anti-patterns to NOT repeat

Discovered the hard way during the 2026-05-22 tracking overhaul. Read before designing any tracking change.

### LL-1. Don't mirror the Explorer pattern blindly on the Creator Hub

The Creator Hub flow is **dispatch-and-forget** (anchor click → 3s setTimeout → redirect). There is no stream, no progress, no failure signal from the browser. PR #619 did add `download_started`/`download_success` for this funnel (see section 6) — but **reusing the shared `SegmentEvent` enum**, not a new `creator_hub_download_*` family, and **without** a `_failed` counterpart (there's nothing to catch a failure from). If a future change proposes a dedicated `CREATOR_HUB_DOWNLOAD_*` enum family or a `_failed` event for this flow, that's the anti-pattern this lesson warns against — confirm with the data team first, don't invent a failure signal that doesn't exist in the browser.

### LL-2. Don't assume an event family is active — check with the data team

The `'Onboarding Checkpoint'` family was deprecated by data team and the entire helper + callsites had to be removed. Before adding any tracking to a "checkpoint"-style funnel, confirm with the data team that they want it. Don't reanimate `trackCheckpoint` — design from scratch.

### LL-3. Payload key conventions (read before adding any track call)

- **`place`** (not `section`, not `location`) — the canonical key for "where the click happened". `InviteHero.tsx` previously used `section: eventPlace` and it broke every analytics query that grouped by `place`. F

…(truncated)
