Skip to content
codefastlabs

Command Palette

Search for a command to run...

@codefast/trackingv0.7.1· Specification

spec-event-model — Catalog, Validation, Envelope

The key words MUST, MUST NOT, SHOULD, and MAY are to be interpreted as described in RFC 2119.

1. Event catalog

An event catalog is an app-defined map from event name to an event definition. The tracking library MUST NOT ship any events of its own — every consuming app builds and passes in its own catalog.

  • An event definition carries, at minimum, a schema for that event's properties.
  • Event names are the catalog's keys; the tracker accepts only names present in the catalog.

The schema mechanism is implementation-defined (the reference implementation uses Standard Schema), but it MUST provide:

  • Synchronous validation. Tracking sits on synchronous call paths; a schema that can only validate asynchronously MUST be rejected with an error at track time — not awaited, not silently skipped.
  • A parsed output. Validation returns the value destinations receive. Unknown keys MUST NOT survive validation (either the schema strips them or the implementation forwards only the parsed output), so arbitrary caller data cannot ride along to third-party sinks.
  • Failure detail. A validation failure MUST raise an error identifying the event name and the schema's issue messages.

2. Property validation

Given (schema, eventName, rawProperties):

  1. Run the schema's synchronous validation on rawProperties.
  2. If the schema reports it validates asynchronously → error: async schemas are unsupported by design.
  3. If validation reports issues → error carrying eventName and the joined issue messages.
  4. Otherwise return the parsed output (transforms applied, unknown keys stripped). Destinations MUST receive this output, never the raw input.

3. The TrackEvent envelope

Every event handed to a destination is one JSON-representable envelope:

{
  "type": "track",
  "name": "newsletter_signup",
  "properties": { "plan": "pro" },
  "anonymousId": "6f1c2a4e-9b0d-4c3e-8f5a-1d2e3c4b5a69",
  "eventId": "0d9b8a7c-6e5f-4d3c-2b1a-9f8e7d6c5b4a",
  "timestamp": 1752796800000
}
Field Type Rules
type string Always "track". A discriminant: a future event kind is an additive union member, never a change to this shape.
name string The catalog key the caller tracked.
properties object The parsed output of schema validation (section 2).
anonymousId string The visitor's anonymous id — or the empty string "" when the consent gate is closed (see spec-tracker §3).
eventId string A freshly generated random UUID (RFC 4122 v4), stamped at track time. Destinations that dedupe MAY key on it.
timestamp number Milliseconds since the Unix epoch, stamped at track time.
  • eventId MUST be unique per track() call — never reused across retries or destinations.
  • The envelope is immutable once built: every destination in a fan-out receives the same envelope value.

4. Reserved: the context envelope

Status: reserved key, not yet populated. Every comparable system (Segment, RudderStack, PostHog, Amplitude, Mixpanel, Snowplow) auto-attaches a context envelope; this system currently omits one. Reserving the key now — without populating it — avoids a breaking envelope change when the first consumer needs attribution, while honoring the standing "real call site before it ships" rule.

An optional context? key MAY carry ambient attribution data. Its shape is a Segment-subset:

context field Contents Why reserved
page url, path, referrer, title, search referrer/URL can't be reconstructed by the tag after client-side navigation
campaign source, medium, name, term, content (UTM) first-touch attribution is lost after the first navigation if not captured at track time
library name, version near-free; invaluable for debugging a misbehaving destination
locale BCP-47 string cheap, standard

ip/geo MUST NOT appear in the client envelope — it is consent-sensitive and belongs to the destination's own server-side derivation.

Consent-first constraint (normative when populated). page.url, page.referrer, and campaign can carry PII and tracking signal, so any context MUST be subject to the same consent gate as the rest of the envelope and stripped in the exempt lane — an exempt (identifier-free) envelope carries anonymousId == "" and no context. This is why the field is specified here rather than copied wholesale from an SDK that ships context unconditionally at init: the consent-first thesis (spec-tracker §3) applies to ambient context exactly as it applies to the identifier.

Conformance vectors

V1 — parsed output forwarded, unknown keys stripped. Schema: { plan: string }. Input properties { "plan": "pro", "injected": "x" } → destinations receive { "plan": "pro" }.

V2 — validation failure. Schema requires plan: string. Input { "plan": 5 } → error whose message contains the event name and the schema's issue message; no destination receives anything.

V3 — async schema rejected. A schema whose validation returns a deferred/promise value → error stating the event uses an async schema; no destination receives anything.

V4 — unknown event name. track("not_in_catalog", …) → error Unknown event: not_in_catalog; no destination receives anything.

V5 — envelope shape. A permitted track("signup", {"plan":"pro"}) with anonymous id A produces an envelope where type == "track", name == "signup", properties == {"plan":"pro"}, anonymousId == A, eventId matches ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ (case-insensitive), and timestamp is the current epoch-milliseconds.

V6 — context stripped in the exempt lane (when §4 is populated). A gated event delivered to an exempt destination carries anonymousId == "" and no context key, even when the same event delivered under an open gate would carry context.

License

Released under the MIT License.