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 viarequestIdleCallbackwith a 4s fallback timer. That means on cold loads,useAnalytics().isInitializedisfalsefor up to ~4s. - Underlying lib:
@dcl/hooks(workspace submodulehooks/) exposesuseAnalytics()which returns{ isInitialized, track, identify, page }. WhenisInitialized === false,trackis a no-op — calls just drop. There is no built-in queueing inuseAnalytics. - Contentsquare: activated alongside Segment via
scheduleDeferredThirdPartyinsrc/modules/deferredThirdParty.ts. Out of scope here; it's session recording, not event tracking. - Anonymous id:
@segment/analytics-nextstores its anonymous id inlocalStorageasajs_anonymous_id(JSON-encoded). Read it viauseAnonUserId()(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.comandapi.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 ascdnUrl) andSEGMENT_API_HOST(api.e.decentraland.org/v1, the proxied Tracking API: SDK delivery plus the base of the beacon's track URL viagetSegmentTrackUrl()). 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 ondecentraland.org. On.zone/.todayit is third-party, so EasyPrivacy's blanket$ping,third-partyrule still dropsnavigator.sendBeaconthere andpostSegmentEventdegrades to itsfetch keepalivefallback.
2. Two abstraction layers — infra and domain
Tracking in sites is split into two layers that compose cleanly:
- Infra (
useDeferredTrack) — wrapsuseAnalytics().trackwith 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. Addstrack_called_at,track_delivered_at,track_deferredto 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 (seepostSegmentEventbelow 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 vianavigator.sendBeacon(falling back tofetch keepalive), bypassinguseAnalytics()/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 identifieduserIditself viaresolveSegmentUserId()(src/modules/segmentUserId.ts, reads the SDK's ownajs_user_idkey) and attaches the SDK-parity context (integrations: {},context.page.search,context.timezone,context.userAgentData). It deliberately omitstype(the/v1/trackendpoint 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 (useDeferredTrackorpostSegmentEvent).
You should almost never call useAnalytics().track directly. The decision tree:
- Click handler driven by
data-*? →useTrackClick(which already usesuseDeferredTrackinternally). - Download funnel event (
download_started/_success/_failed)? →createDownloadTracker(ctx)— fires viapostSegmentEvent(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, followingdownloadTracking.ts/downloadFunnelExit.ts. Don't reach foruseAnalytics().trackdirectly 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 withtrack(). Mixed casing because some literals are historical:'Click','Download','Reels Click …'(Title Case Words), and the funnel eventsdownload_started / download_success / download_failed(snake_case). Don't normalize — the data team tracks by these literal strings.DownloadPlace— kebab-case enum for theplacefield ofdownload_*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— seesrc/modules/segment.types.tsfor the authoritative list (it grows; don't copy this one stale).SectionViewedTrack— Title Case enum for theplacefield ofClickevents (consumed viadata-place). Values:Landing Hero,Creators Hero,Landing Explore, etc. Different namespace fromDownloadPlace— they happen to overlap in intent (e.g.SectionViewedTrack.LANDING_HERO = 'Landing Hero'vsDownloadPlace.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-eventMUST be aSegmentEventenum value, never a hardcoded literal. The lowercase"click"literal is no longer accepted as a special value — usedata-event={SegmentEvent.CLICK}if you want to be explicit.- If
data-eventequalsSegmentEvent.CLICK, the adapter strips it from the payload (would otherwise duplicate the event name). data-placeis the canonical key for "where the click happened" — useSectionViewedTrack.Xenum values.
Examples in current code:
data-event={SegmentEvent.DOWNLOAD}→ Hero Download / Epic / Create/Hero (primary + secondaries) / CreatorHubDownload (primary + secondaries) / DownloadSuccess footer. Payload includesevent: 'Download'.data-event={SegmentEvent.REELS_CLICK_DCL_LOGO}→ Reels Logo. Payload includesevent: 'Reels Click Decentraland Logo'.data-event={SegmentEvent.CLICK}→ WhatsOn / WhatsOnCard / WeeklyRituals / ComeHangOut / CatchTheVibe / JumpIn (via CTAButton default). Payload does NOT includeevent(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 butplace = 'download-success-footer'is hardcoded.
Payload shape (both flows, see src/modules/downloadTracking.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:
_STARTEDfires beforestreamOrFallback(intent signal). If user closes the tab during the stream →_STARTEDis in the warehouse, no_SUCCESS/_FAILED. Pair-wise diffcount(_STARTED) - count(_SUCCESS) - count(_FAILED)≈ abandon rate._SUCCESSfires afterstreamOrFallbackresolves. 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 withoutshowSaveFilePicker, which is Chromium-only and breaks Safari/Firefox and the macOSkMDItemWhereFromsattribution. Theosfield in the payload tells consumers which semantic applies._FAILEDfires in the.catch()branch. If the failure is incalculateDownloadUrl(i.e. before the tracker was built), a fallback tracker is constructed in the catch withhref = 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 offsource(defaults towindow.location.search, SSR-guarded), truncates each value toMAX_CAMPAIGN_VALUE_LENGTH = 256chars, omits absent/empty ones.withCampaignParams(path)— appends the currently-collected params to a path. Used for the/downloadfallback href rendered beforeuserAgentDataresolves (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 timecollectCampaignParams()runs. Partner links must point directly at a page hosting download CTAs (/,/download,/play). - Kept snake_case (
utm_source, notutmSource) 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):
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):
{
...(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-hubpage) emitClickwithplace=Creators Hero/place=Downloadandevent=Downloadvia the standarduseTrackClickadapter, same as any other download surface. download_started— fired fromsrc/hooks/useCreatorHubDownload.ts(handleDownload) viacreateDownloadTracker(...).started()at the moment the file download is triggered (place: DownloadPlace.CREATOR_HUB_DOWNLOAD_PAGE).revisitis hardcoded0— a click is a one-shot intent, there's no per-attempt revisit notion on this page.download_success— fired fromsrc/pages/download/CreatorHubDownloadSuccess.tsxon mount (useEffect, guarded by a ref so React strict-mode double-invoke only fires once) viacreateDownloadTracker(...).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.revisitincrements per mount for the sameos:arch(sessionStorage counter, mirrors the ExplorerDownloadSuccesspattern).- No
download_failed— still true, and still by design: the Creator Hub download isdispatch-and-forget(triggerFileDownload+ a 3ssetTimeoutredirect, no stream to observe). There is no browser-observable failure signal to fire it from. - The footer re-download on
/download/creator-hub-successfiresClickvia the standarduseTrackClickadapter withdata-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)insrc/components/Layout/Layout.tsx(~line 16), runs on every route change unlessisPageTrackingExempt(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/*) useuseBlogPageTrackingto 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
# 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
- 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 forDownloadPlace-like dimensions). - Decide the transport: is the event fired from a click-handled DOM element?
useTrackClickviadata-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 auseEffectthat already awaits something Segment-y)?useAnalyticsis fine. - 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
undefinedrather than sending null — keeps warehouse rows cleaner.
- snake_case keys (the codebase has eslint-disable comments for
- Tests: add a unit test that asserts both the event name and the payload shape. See
src/modules/downloadTracking.spec.tsfor a per-event matcher pattern andsrc/pages/DownloadSuccess/DownloadSuccess.spec.tsxfor an integration shape with mocked hooks. - 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):
useTrackClickignoringdata-eventfor 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
trackCheckpointhelper were removed. No replacement scheduled. - P0-3 (✅ done — PR #619): Creator Hub now fires
download_started(on click) anddownload_success(on success-page mount) via the sharedcreateDownloadTracker, reusingSegmentEvent.DOWNLOAD_STARTED/_SUCCESS— NOT a separateCREATOR_HUB_DOWNLOAD_*enum family. No_FAILED(still no observable failure signal). See section 6. - P1-1 (✅ done):
download_started/success/failedpayload + timing fixes. See Plan.md section. - P1-2 (✅ done —
useAnonUserIdreactivity 2026-05-22): hook now depends onisInitializedso it re-evaluates when Segment boots;DownloadSuccessgates the auto-download on aanonUserIdReadystate with an 800ms timeout fallback. See LL-9. - P1-3 (✅ partial via
useDeferredTrack):useTrackClicksilent drop whenisInitialized === false. AdoptinguseDeferredTrackinside the adapter would resolve this for Click events too. - P1-4 (✅ done —
download_started/_success/_faileddrop-on-unmount fixed 2026-07-01): these events used to fire viauseDeferredTrack, 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.createDownloadTrackernow fires viapostSegmentEvent+ensureSegmentAnonymousId()instead, matching theuseDownloadClick(PR #636) /downloadFunnelExit.ts(PR #632) precedent. See 5.3 and LL-10. - P2-4 (✅ done — PR #668):
DownloadSuccess.tsxnow catches onlycollectClientFingerprint()insidebuildTrackerExtra(). Campaign params, click correlation, anddownload_targetare 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(notsection, notlocation) — the canonical key for "where the click happened".InviteHero.tsxpreviously usedsection: eventPlaceand it broke every analytics query that grouped byplace. F
…(truncated)