@usefidel/contracts 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,14 +6,29 @@
6
6
  * (webapp, landing).
7
7
  *
8
8
  * WHY THIS IS A PACKAGE AND NOT A LOCAL FILE PER SURFACE.
9
- * The webapp and the extension each identified the same human under a
10
- * different PostHog `distinct_id` the webapp by Supabase UUID-or-email, the
11
- * extension by email-or-Figma-handle. `extension/src/lib/posthog.ts` carried a
12
- * comment asserting the two agreed. They did not. Nothing failed; retention
13
- * cohorts were simply wrong, and stayed wrong, because the two "authoritative"
14
- * definitions lived in two repositories with no gate between them.
15
- *
16
- * The package is the only mechanism that spans both repositories.
9
+ * The webapp and the extension each wrote their own identity rule, and the two
10
+ * rules were written to agree. In the common case they did: both normally
11
+ * identified a person by email.
12
+ *
13
+ * The defect was in what happened around that agreement.
14
+ *
15
+ * 1. EMAIL IS MUTABLE. A `distinct_id` must be stable for the life of the
16
+ * person, and an email address is not changing it forks one human into
17
+ * two identities with no event connecting them.
18
+ * 2. THE FALLBACKS DIVERGED. Email is not always available, and each surface
19
+ * fell back into a DIFFERENT namespace when it was missing — a Supabase
20
+ * UUID on one side, a Figma handle on the other. So the same human was
21
+ * identified consistently right up until the moment they were not, which
22
+ * is the hardest version of this bug to see.
23
+ * 3. NOTHING GATED THE DUPLICATE. The rule was written out twice, in two
24
+ * repositories, with no check that the two copies still said the same
25
+ * thing. A drift would have been silent — and an earlier comment in
26
+ * `extension/src/lib/posthog.ts` asserting the two agreed is exactly the
27
+ * kind of claim a gate exists to replace.
28
+ *
29
+ * Nothing failed loudly. Retention cohorts were simply wrong, and stayed wrong.
30
+ * The package is the only mechanism that spans both repositories, so it is the
31
+ * only place the rule can be stated once.
17
32
  *
18
33
  * WHAT THIS OWNS: event NAMES and property TYPES.
19
34
  * WHAT IT DOES NOT OWN: SDK initialization, identity resolution, storage,
@@ -42,7 +57,21 @@ import { type RunErrorCode } from './run-errors.js';
42
57
  * history survives; the meaning of their events does not.
43
58
  */
44
59
  export declare const ANALYTICS_SCHEMA_VERSION: 2;
45
- /** Which client emitted the event. Server-side surfaces do not emit at all. */
60
+ /**
61
+ * Which PRODUCT JOURNEY the event belongs to — not which process emitted it.
62
+ *
63
+ * This comment used to read "server-side surfaces do not emit at all", and that
64
+ * stopped being true when authoritative terminal events moved into the
65
+ * `web-validate` finalizer. A validation started from the web app is a webapp
66
+ * validation whether the browser or the backend reports its outcome, so the
67
+ * backend's `web_validation_completed` correctly carries `surface: 'webapp'`.
68
+ *
69
+ * Making the backend its own surface would split one funnel in half: the
70
+ * submission would sit under `webapp` and its completion under something else,
71
+ * and no tile could relate them without a union. Which process emitted an event
72
+ * is answered by the event NAME and by `release`, both of which already
73
+ * distinguish the two.
74
+ */
46
75
  export type AnalyticsSurface = 'landing' | 'webapp' | 'extension';
47
76
  /** Which deployment emitted the event. Drives the default dashboard filter. */
48
77
  export type AnalyticsEnvironment = 'production' | 'staging' | 'development';
@@ -130,13 +159,16 @@ export type GaEventName = (typeof GA_EVENT_NAMES)[number];
130
159
  export type SecondaryEventName = (typeof SECONDARY_EVENT_NAMES)[number];
131
160
  export type AnalyticsEventName = GaEventName | SecondaryEventName;
132
161
  /**
133
- * Stamped on every schema-2 event by the wrapper, not by call sites.
162
+ * Stamped on every schema-2 event by the emitter, not by call sites — the
163
+ * browser wrapper for client events, `_shared/analytics.ts` for the backend
164
+ * terminal events.
134
165
  *
135
- * `team_id` and `is_internal` are optional because the extension cannot get
136
- * them without a network request it is not allowed to make: `team_id` lives in
137
- * `profiles`, not in the Supabase JWT. Team-level analysis uses
166
+ * `fidel_team_id` and `is_internal` are optional because the extension cannot
167
+ * get them without a network request it is not allowed to make: the team lives
168
+ * in `profiles`, not in the Supabase JWT. Team-level analysis uses
138
169
  * `validation_runs.team_id` in the database instead. The webapp has both and
139
- * always sends them.
170
+ * always sends them, and the backend reads both from the resolved team
171
+ * context it already loaded to run the subscription gate.
140
172
  *
141
173
  * There is no `user_id` field. The Supabase UUID is the `distinct_id`;
142
174
  * duplicating it as a property invites two sources of truth for identity.
@@ -147,7 +179,23 @@ export interface AnalyticsContext {
147
179
  surface: AnalyticsSurface;
148
180
  /** Git SHA (webapp/landing) or `manifest.json` version (extension). */
149
181
  release: string;
150
- team_id?: string;
182
+ /**
183
+ * The Fidel team, PREFIXED — and the prefix is not cosmetic.
184
+ *
185
+ * `team_id` is a reserved top-level column on PostHog's own events table (it
186
+ * holds the PostHog project id). A property of that name is still stored in
187
+ * the payload, but `properties.team_id` resolves to the reserved column and
188
+ * returns NULL, so every dashboard filter and breakdown built on it silently
189
+ * matches nothing. Measured on staging 2026-09-08:
190
+ *
191
+ * JSONExtractString(properties,'team_id') -> b34bb03c-… (the real value)
192
+ * properties.team_id -> NULL
193
+ * team_id -> 330851 (PostHog's project)
194
+ *
195
+ * That is the worst failure shape available: instrumented, ingested, and
196
+ * invisible. `is_internal` does not collide and keeps its plain name.
197
+ */
198
+ fidel_team_id?: string;
151
199
  is_internal?: boolean;
152
200
  }
153
201
  /**
package/dist/analytics.js CHANGED
@@ -6,14 +6,29 @@
6
6
  * (webapp, landing).
7
7
  *
8
8
  * WHY THIS IS A PACKAGE AND NOT A LOCAL FILE PER SURFACE.
9
- * The webapp and the extension each identified the same human under a
10
- * different PostHog `distinct_id` the webapp by Supabase UUID-or-email, the
11
- * extension by email-or-Figma-handle. `extension/src/lib/posthog.ts` carried a
12
- * comment asserting the two agreed. They did not. Nothing failed; retention
13
- * cohorts were simply wrong, and stayed wrong, because the two "authoritative"
14
- * definitions lived in two repositories with no gate between them.
9
+ * The webapp and the extension each wrote their own identity rule, and the two
10
+ * rules were written to agree. In the common case they did: both normally
11
+ * identified a person by email.
15
12
  *
16
- * The package is the only mechanism that spans both repositories.
13
+ * The defect was in what happened around that agreement.
14
+ *
15
+ * 1. EMAIL IS MUTABLE. A `distinct_id` must be stable for the life of the
16
+ * person, and an email address is not — changing it forks one human into
17
+ * two identities with no event connecting them.
18
+ * 2. THE FALLBACKS DIVERGED. Email is not always available, and each surface
19
+ * fell back into a DIFFERENT namespace when it was missing — a Supabase
20
+ * UUID on one side, a Figma handle on the other. So the same human was
21
+ * identified consistently right up until the moment they were not, which
22
+ * is the hardest version of this bug to see.
23
+ * 3. NOTHING GATED THE DUPLICATE. The rule was written out twice, in two
24
+ * repositories, with no check that the two copies still said the same
25
+ * thing. A drift would have been silent — and an earlier comment in
26
+ * `extension/src/lib/posthog.ts` asserting the two agreed is exactly the
27
+ * kind of claim a gate exists to replace.
28
+ *
29
+ * Nothing failed loudly. Retention cohorts were simply wrong, and stayed wrong.
30
+ * The package is the only mechanism that spans both repositories, so it is the
31
+ * only place the rule can be stated once.
17
32
  *
18
33
  * WHAT THIS OWNS: event NAMES and property TYPES.
19
34
  * WHAT IT DOES NOT OWN: SDK initialization, identity resolution, storage,
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "The source of truth still lives in the monorepo at packages/contracts/."
8
8
  ],
9
9
  "name": "@usefidel/contracts",
10
- "version": "0.11.0",
10
+ "version": "0.12.0",
11
11
  "description": "Shared, code-free contracts between Fidel surfaces. Run-error taxonomy, theme-intake wire types, and the canonical fidel.config.json builders.",
12
12
  "license": "UNLICENSED",
13
13
  "private": false,