@usefidel/contracts 0.6.0 → 0.7.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.
- package/dist/analytics.d.ts +340 -0
- package/dist/analytics.js +241 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/package.json +5 -1
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics contract — GA Phase 1.
|
|
3
|
+
*
|
|
4
|
+
* ONE vocabulary for client analytics event names and their property shapes,
|
|
5
|
+
* shared by `usefidel/usefidel` (extension, admin) and `usefidel/fidel-web`
|
|
6
|
+
* (webapp, landing).
|
|
7
|
+
*
|
|
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.
|
|
17
|
+
*
|
|
18
|
+
* WHAT THIS OWNS: event NAMES and property TYPES.
|
|
19
|
+
* WHAT IT DOES NOT OWN: SDK initialization, identity resolution, storage,
|
|
20
|
+
* environment detection, redaction, or any `posthog.*` call. Those stay in a
|
|
21
|
+
* per-surface wrapper, because they differ per surface in ways a shared module
|
|
22
|
+
* would have to branch on — and a shared module full of surface branches is
|
|
23
|
+
* how the last divergence started.
|
|
24
|
+
*
|
|
25
|
+
* NARROWNESS (see index.ts). This file adds runtime values — two frozen name
|
|
26
|
+
* arrays and one error-code-to-phase map. That is the same shape as
|
|
27
|
+
* `RUN_ERROR_CODES`, which this package already publishes. It carries no
|
|
28
|
+
* engine, no endpoint, no credential, and no backend logic. Event names
|
|
29
|
+
* describe which buttons exist, which a contractor installing the package can
|
|
30
|
+
* already observe by using the product.
|
|
31
|
+
*/
|
|
32
|
+
import { type RunErrorCode } from './run-errors.js';
|
|
33
|
+
/**
|
|
34
|
+
* Schema version stamped on every event emitted by a contract-aware wrapper.
|
|
35
|
+
*
|
|
36
|
+
* Version 1 is everything emitted before the GA cutover: mixed identity,
|
|
37
|
+
* success events that fired before the operation succeeded, camelCase and
|
|
38
|
+
* snake_case properties side by side. Version 2 is this contract.
|
|
39
|
+
*
|
|
40
|
+
* The versions are NOT semantically comparable and must never be summed in one
|
|
41
|
+
* tile. Identity is preserved across the cutover by aliasing, so a person's
|
|
42
|
+
* history survives; the meaning of their events does not.
|
|
43
|
+
*/
|
|
44
|
+
export declare const ANALYTICS_SCHEMA_VERSION: 2;
|
|
45
|
+
/** Which client emitted the event. Server-side surfaces do not emit at all. */
|
|
46
|
+
export type AnalyticsSurface = 'landing' | 'webapp' | 'extension';
|
|
47
|
+
/** Which deployment emitted the event. Drives the default dashboard filter. */
|
|
48
|
+
export type AnalyticsEnvironment = 'production' | 'staging' | 'development';
|
|
49
|
+
/**
|
|
50
|
+
* Validation mode.
|
|
51
|
+
*
|
|
52
|
+
* Mirrors the `validation_runs.validation_mode` CHECK constraint —
|
|
53
|
+
* migration 096 established `figma_vs_live` / `url_vs_url`, migration 098
|
|
54
|
+
* added `design_system_vs_live`. Kept in step by `check-canonical-sync.mjs`.
|
|
55
|
+
*/
|
|
56
|
+
export type ValidationMode = 'figma_vs_live' | 'url_vs_url' | 'design_system_vs_live';
|
|
57
|
+
/**
|
|
58
|
+
* How much of the required scope Fidel actually inspected.
|
|
59
|
+
*
|
|
60
|
+
* STRUCTURAL COPY of `EvidenceState` in
|
|
61
|
+
* `supabase/functions/_shared/result-completeness.ts` (plan 051 P1-5), which
|
|
62
|
+
* is the canonical definition and is vendor-synced to the Lambda and the
|
|
63
|
+
* marketplace Action. `check-canonical-sync.mjs` asserts this union matches
|
|
64
|
+
* that one exactly.
|
|
65
|
+
*
|
|
66
|
+
* Present ONLY on `design_system_vs_live` runs. Absent on every other mode by
|
|
67
|
+
* design — see `WebValidationCompletedProperties.result_completeness`.
|
|
68
|
+
*/
|
|
69
|
+
export type ResultCompleteness = 'complete' | 'partial' | 'unverified' | 'configuration_required' | 'operational_failure';
|
|
70
|
+
/**
|
|
71
|
+
* Which stage of a run failed.
|
|
72
|
+
*
|
|
73
|
+
* DERIVED, never stored. `validation_runs` has `error_code` but no
|
|
74
|
+
* `error_phase` column, so phase is computed from the code by
|
|
75
|
+
* `errorPhaseFor()` below. That keeps one source of truth (the taxonomy) and
|
|
76
|
+
* means admin can group failures by phase today without a migration.
|
|
77
|
+
*/
|
|
78
|
+
export type ValidationErrorPhase =
|
|
79
|
+
/** The submitted URLs were rejected before any work started. */
|
|
80
|
+
'input_validation'
|
|
81
|
+
/** Could not reach, load, or authenticate against the live target. */
|
|
82
|
+
| 'target_capture'
|
|
83
|
+
/** Could not read the Figma file. */
|
|
84
|
+
| 'figma_fetch'
|
|
85
|
+
/** The design system or brand context could not be resolved. */
|
|
86
|
+
| 'design_system_resolve'
|
|
87
|
+
/** The matching pipeline ran and failed, or produced nothing usable. */
|
|
88
|
+
| 'pipeline'
|
|
89
|
+
/** The run completed but the result could not be written. */
|
|
90
|
+
| 'persist'
|
|
91
|
+
/** Unclassified. A growing count here means the taxonomy needs a code. */
|
|
92
|
+
| 'unknown';
|
|
93
|
+
/**
|
|
94
|
+
* Every run error code, mapped to the phase it belongs to.
|
|
95
|
+
*
|
|
96
|
+
* EXHAUSTIVE BY CONSTRUCTION: the `Record<RunErrorCode, …>` type means adding
|
|
97
|
+
* a code to the taxonomy without assigning it a phase is a compile error in
|
|
98
|
+
* this package, which is a merge gate in both repositories. That is deliberate
|
|
99
|
+
* — the alternative is a default branch, and a default branch silently files
|
|
100
|
+
* every new code under `unknown`.
|
|
101
|
+
*/
|
|
102
|
+
export declare const ERROR_CODE_PHASE: Record<RunErrorCode, ValidationErrorPhase>;
|
|
103
|
+
/**
|
|
104
|
+
* Phase for a code, tolerating values from outside the taxonomy.
|
|
105
|
+
*
|
|
106
|
+
* The admin dashboard reads `error_code` out of Postgres, where it is a bare
|
|
107
|
+
* TEXT column with no FK to the taxonomy. A row written by an older deploy can
|
|
108
|
+
* therefore hold a code this build has never heard of. Returning `'unknown'`
|
|
109
|
+
* for that is right; throwing would take down the tile.
|
|
110
|
+
*/
|
|
111
|
+
export declare function errorPhaseFor(code: string | null | undefined): ValidationErrorPhase;
|
|
112
|
+
/** True when `code` is a member of the current taxonomy. */
|
|
113
|
+
export declare function isKnownErrorCode(code: string | null | undefined): code is RunErrorCode;
|
|
114
|
+
/** Events the GA dashboard is built on. Typed properties, specified triggers. */
|
|
115
|
+
export declare const GA_EVENT_NAMES: readonly ["signup_started", "signup_completed", "onboarding_started", "onboarding_completed", "figma_connected", "environment_connected", "web_validation_started", "web_validation_completed", "web_validation_failed", "web_validation_cancelled", "report_viewed", "report_shared", "diff_mark_resolved", "upgrade_clicked", "checkout_started", "feedback_submitted", "extension_opened", "validation_start", "validation_accepted", "validation_complete", "validation_error", "validation_cancel", "review_saved", "figma_auth_success", "figma_auth_error", "extension_share_copied", "upgrade_modal_viewed", "upgrade_cta_clicked"];
|
|
116
|
+
/**
|
|
117
|
+
* Events that exist but are NOT GA metrics.
|
|
118
|
+
*
|
|
119
|
+
* `checkout_completed` is here rather than in `GA_EVENT_NAMES` deliberately.
|
|
120
|
+
* It fires in the browser on return from Stripe, which is neither reliable
|
|
121
|
+
* (the tab can be closed) nor authoritative (the webhook is). Paid conversion
|
|
122
|
+
* comes from `teams.subscription_status` and Stripe. This event is a funnel
|
|
123
|
+
* diagnostic for "did the user come back", nothing more, and must be labelled
|
|
124
|
+
* as such wherever it appears.
|
|
125
|
+
*/
|
|
126
|
+
export declare const SECONDARY_EVENT_NAMES: readonly ["checkout_completed", "console_errors", "diff_expanded", "drawer_filter_changed", "review_save_error", "review_verdict", "review_verdict_all", "screenshot_exported", "ui_crash", "validation_resume", "validation_retry", "view_changed", "auth_refresh", "consensus_complete", "consensus_error", "extension_validation_failed", "idempotency_dedup_client", "pipeline_complete", "pipeline_phase", "pipeline_resume", "rate_limit_hit", "upgrade_wall_hit"];
|
|
127
|
+
/** Every event name any Fidel client may emit. */
|
|
128
|
+
export declare const ANALYTICS_EVENT_NAMES: readonly ["signup_started", "signup_completed", "onboarding_started", "onboarding_completed", "figma_connected", "environment_connected", "web_validation_started", "web_validation_completed", "web_validation_failed", "web_validation_cancelled", "report_viewed", "report_shared", "diff_mark_resolved", "upgrade_clicked", "checkout_started", "feedback_submitted", "extension_opened", "validation_start", "validation_accepted", "validation_complete", "validation_error", "validation_cancel", "review_saved", "figma_auth_success", "figma_auth_error", "extension_share_copied", "upgrade_modal_viewed", "upgrade_cta_clicked", "checkout_completed", "console_errors", "diff_expanded", "drawer_filter_changed", "review_save_error", "review_verdict", "review_verdict_all", "screenshot_exported", "ui_crash", "validation_resume", "validation_retry", "view_changed", "auth_refresh", "consensus_complete", "consensus_error", "extension_validation_failed", "idempotency_dedup_client", "pipeline_complete", "pipeline_phase", "pipeline_resume", "rate_limit_hit", "upgrade_wall_hit"];
|
|
129
|
+
export type GaEventName = (typeof GA_EVENT_NAMES)[number];
|
|
130
|
+
export type SecondaryEventName = (typeof SECONDARY_EVENT_NAMES)[number];
|
|
131
|
+
export type AnalyticsEventName = GaEventName | SecondaryEventName;
|
|
132
|
+
/**
|
|
133
|
+
* Stamped on every schema-2 event by the wrapper, not by call sites.
|
|
134
|
+
*
|
|
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
|
|
138
|
+
* `validation_runs.team_id` in the database instead. The webapp has both and
|
|
139
|
+
* always sends them.
|
|
140
|
+
*
|
|
141
|
+
* There is no `user_id` field. The Supabase UUID is the `distinct_id`;
|
|
142
|
+
* duplicating it as a property invites two sources of truth for identity.
|
|
143
|
+
*/
|
|
144
|
+
export interface AnalyticsContext {
|
|
145
|
+
analytics_schema_version: typeof ANALYTICS_SCHEMA_VERSION;
|
|
146
|
+
environment: AnalyticsEnvironment;
|
|
147
|
+
surface: AnalyticsSurface;
|
|
148
|
+
/** Git SHA (webapp/landing) or `manifest.json` version (extension). */
|
|
149
|
+
release: string;
|
|
150
|
+
team_id?: string;
|
|
151
|
+
is_internal?: boolean;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Person properties. Set on identify, never sent as event properties.
|
|
155
|
+
*
|
|
156
|
+
* `email` is a person property and not a `distinct_id` — that distinction is
|
|
157
|
+
* the entire identity migration. See plan §3.
|
|
158
|
+
*/
|
|
159
|
+
export interface AnalyticsPersonProperties {
|
|
160
|
+
email?: string;
|
|
161
|
+
figma_handle?: string;
|
|
162
|
+
team_id?: string;
|
|
163
|
+
plan?: string;
|
|
164
|
+
subscription_status?: string;
|
|
165
|
+
is_internal?: boolean;
|
|
166
|
+
}
|
|
167
|
+
/** Properties common to every validation event, on both clients. */
|
|
168
|
+
export interface ValidationEventProperties {
|
|
169
|
+
/** `validation_runs.id`. The join key across PostHog, Postgres and Sentry. */
|
|
170
|
+
run_id: string;
|
|
171
|
+
validation_mode: ValidationMode;
|
|
172
|
+
}
|
|
173
|
+
export interface ValidationStartedProperties extends ValidationEventProperties {
|
|
174
|
+
viewport?: string;
|
|
175
|
+
is_revalidation?: boolean;
|
|
176
|
+
}
|
|
177
|
+
export interface WebValidationCompletedProperties extends ValidationEventProperties {
|
|
178
|
+
duration_ms: number;
|
|
179
|
+
/**
|
|
180
|
+
* Confirmed violations, per the result contract.
|
|
181
|
+
*
|
|
182
|
+
* For `design_system_vs_live` this is `driftCount + offTokenCount` and is
|
|
183
|
+
* `null` when either half is unavailable. It is NEVER `diffs.length`:
|
|
184
|
+
* that array also holds `unused` and `scoped` findings, and printing it as a
|
|
185
|
+
* violation count is the exact defect plan 051 P1-5 was written to close.
|
|
186
|
+
*
|
|
187
|
+
* `null` means "the producer told us nothing", which is not zero.
|
|
188
|
+
*/
|
|
189
|
+
violation_count: number | null;
|
|
190
|
+
/**
|
|
191
|
+
* ABSENT on `figma_vs_live` and `url_vs_url` runs — the pipeline only emits
|
|
192
|
+
* a completeness block for `design_system_vs_live`. Absence here is correct
|
|
193
|
+
* behaviour and not an instrumentation failure; any tile using it must be
|
|
194
|
+
* scoped to DS runs.
|
|
195
|
+
*/
|
|
196
|
+
result_completeness?: ResultCompleteness;
|
|
197
|
+
}
|
|
198
|
+
export interface ValidationFailedProperties extends ValidationEventProperties {
|
|
199
|
+
duration_ms: number;
|
|
200
|
+
error_code: string;
|
|
201
|
+
error_phase: ValidationErrorPhase;
|
|
202
|
+
retryable: boolean;
|
|
203
|
+
}
|
|
204
|
+
export interface ValidationCancelledProperties extends ValidationEventProperties {
|
|
205
|
+
duration_ms: number;
|
|
206
|
+
error_phase: ValidationErrorPhase;
|
|
207
|
+
}
|
|
208
|
+
export interface ReportViewedProperties {
|
|
209
|
+
run_id: string;
|
|
210
|
+
validation_mode: ValidationMode;
|
|
211
|
+
finding_count: number;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The exhaustive event-to-property mapping.
|
|
215
|
+
*
|
|
216
|
+
* Secondary events map to a permissive record: they predate this contract and
|
|
217
|
+
* typing their existing payloads would be a rename exercise with no consumer.
|
|
218
|
+
* They are still name-checked, which is what the drift gate needs.
|
|
219
|
+
*/
|
|
220
|
+
export interface AnalyticsEventProperties {
|
|
221
|
+
signup_started: {
|
|
222
|
+
plan?: string;
|
|
223
|
+
};
|
|
224
|
+
signup_completed: {
|
|
225
|
+
method: string;
|
|
226
|
+
confirmation_required: boolean;
|
|
227
|
+
plan?: string;
|
|
228
|
+
};
|
|
229
|
+
onboarding_started: Record<string, never>;
|
|
230
|
+
onboarding_completed: {
|
|
231
|
+
completion_path: string;
|
|
232
|
+
figma_connected: boolean;
|
|
233
|
+
environment_connected: boolean;
|
|
234
|
+
};
|
|
235
|
+
figma_connected: {
|
|
236
|
+
method: 'oauth' | 'pat';
|
|
237
|
+
is_first_connect: boolean;
|
|
238
|
+
};
|
|
239
|
+
environment_connected: {
|
|
240
|
+
connection_type: string;
|
|
241
|
+
};
|
|
242
|
+
web_validation_started: ValidationStartedProperties;
|
|
243
|
+
web_validation_completed: WebValidationCompletedProperties;
|
|
244
|
+
web_validation_failed: ValidationFailedProperties;
|
|
245
|
+
web_validation_cancelled: ValidationCancelledProperties;
|
|
246
|
+
report_viewed: ReportViewedProperties;
|
|
247
|
+
report_shared: {
|
|
248
|
+
run_id: string;
|
|
249
|
+
};
|
|
250
|
+
diff_mark_resolved: {
|
|
251
|
+
run_id: string;
|
|
252
|
+
review_action: string;
|
|
253
|
+
};
|
|
254
|
+
upgrade_clicked: {
|
|
255
|
+
plan: string;
|
|
256
|
+
location: string;
|
|
257
|
+
};
|
|
258
|
+
checkout_started: {
|
|
259
|
+
plan: string;
|
|
260
|
+
interval: 'monthly' | 'annual';
|
|
261
|
+
};
|
|
262
|
+
feedback_submitted: {
|
|
263
|
+
feedback_type: string;
|
|
264
|
+
};
|
|
265
|
+
extension_opened: Record<string, never>;
|
|
266
|
+
/** User intent — the Validate button was pressed. Not an accepted run. */
|
|
267
|
+
validation_start: {
|
|
268
|
+
validation_mode: ValidationMode;
|
|
269
|
+
};
|
|
270
|
+
/** The backend accepted the run and issued a run id. The GA "started" event. */
|
|
271
|
+
validation_accepted: ValidationStartedProperties;
|
|
272
|
+
validation_complete: WebValidationCompletedProperties;
|
|
273
|
+
validation_error: ValidationFailedProperties;
|
|
274
|
+
validation_cancel: ValidationCancelledProperties;
|
|
275
|
+
review_saved: {
|
|
276
|
+
run_id?: string;
|
|
277
|
+
review_action: string;
|
|
278
|
+
};
|
|
279
|
+
figma_auth_success: Record<string, never>;
|
|
280
|
+
figma_auth_error: {
|
|
281
|
+
error: string;
|
|
282
|
+
};
|
|
283
|
+
extension_share_copied: {
|
|
284
|
+
run_id?: string;
|
|
285
|
+
};
|
|
286
|
+
upgrade_modal_viewed: {
|
|
287
|
+
plan: string;
|
|
288
|
+
};
|
|
289
|
+
upgrade_cta_clicked: {
|
|
290
|
+
plan: string;
|
|
291
|
+
location: string;
|
|
292
|
+
};
|
|
293
|
+
checkout_completed: {
|
|
294
|
+
plan: string;
|
|
295
|
+
subscription_status: string;
|
|
296
|
+
};
|
|
297
|
+
console_errors: Record<string, unknown>;
|
|
298
|
+
diff_expanded: Record<string, unknown>;
|
|
299
|
+
drawer_filter_changed: Record<string, unknown>;
|
|
300
|
+
review_save_error: Record<string, unknown>;
|
|
301
|
+
review_verdict: Record<string, unknown>;
|
|
302
|
+
review_verdict_all: Record<string, unknown>;
|
|
303
|
+
screenshot_exported: Record<string, unknown>;
|
|
304
|
+
ui_crash: Record<string, unknown>;
|
|
305
|
+
validation_resume: Record<string, unknown>;
|
|
306
|
+
validation_retry: Record<string, unknown>;
|
|
307
|
+
view_changed: Record<string, unknown>;
|
|
308
|
+
auth_refresh: Record<string, unknown>;
|
|
309
|
+
consensus_complete: Record<string, unknown>;
|
|
310
|
+
consensus_error: Record<string, unknown>;
|
|
311
|
+
extension_validation_failed: Record<string, unknown>;
|
|
312
|
+
idempotency_dedup_client: Record<string, unknown>;
|
|
313
|
+
pipeline_complete: Record<string, unknown>;
|
|
314
|
+
pipeline_phase: Record<string, unknown>;
|
|
315
|
+
pipeline_resume: Record<string, unknown>;
|
|
316
|
+
rate_limit_hit: Record<string, unknown>;
|
|
317
|
+
upgrade_wall_hit: Record<string, unknown>;
|
|
318
|
+
}
|
|
319
|
+
/** Payload for one event: its own properties plus the wrapper's context. */
|
|
320
|
+
export type AnalyticsPayload<E extends AnalyticsEventName> = AnalyticsEventProperties[E] & AnalyticsContext;
|
|
321
|
+
/**
|
|
322
|
+
* Property names that must never reach an analytics backend, at any value.
|
|
323
|
+
*
|
|
324
|
+
* This is a NAME denylist and therefore a backstop, not the control. The
|
|
325
|
+
* control is that call sites do not pass these. It exists because the cost of
|
|
326
|
+
* one leaked token is unbounded and the cost of a `Set.has()` is not.
|
|
327
|
+
*
|
|
328
|
+
* Design-token names and CSS custom-property names are NOT credentials and are
|
|
329
|
+
* deliberately absent from this list — they are frequently the only useful
|
|
330
|
+
* debugging signal on a DS run.
|
|
331
|
+
*/
|
|
332
|
+
export declare const FORBIDDEN_PROPERTY_KEYS: readonly string[];
|
|
333
|
+
/**
|
|
334
|
+
* Strip forbidden keys and `undefined` values from a property bag.
|
|
335
|
+
*
|
|
336
|
+
* `undefined` is dropped rather than sent because PostHog stores it as a real
|
|
337
|
+
* property value, which then shows up in the UI as an existing-but-empty
|
|
338
|
+
* property and makes "is not set" filters wrong.
|
|
339
|
+
*/
|
|
340
|
+
export declare function sanitizeProperties(props: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics contract — GA Phase 1.
|
|
3
|
+
*
|
|
4
|
+
* ONE vocabulary for client analytics event names and their property shapes,
|
|
5
|
+
* shared by `usefidel/usefidel` (extension, admin) and `usefidel/fidel-web`
|
|
6
|
+
* (webapp, landing).
|
|
7
|
+
*
|
|
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.
|
|
17
|
+
*
|
|
18
|
+
* WHAT THIS OWNS: event NAMES and property TYPES.
|
|
19
|
+
* WHAT IT DOES NOT OWN: SDK initialization, identity resolution, storage,
|
|
20
|
+
* environment detection, redaction, or any `posthog.*` call. Those stay in a
|
|
21
|
+
* per-surface wrapper, because they differ per surface in ways a shared module
|
|
22
|
+
* would have to branch on — and a shared module full of surface branches is
|
|
23
|
+
* how the last divergence started.
|
|
24
|
+
*
|
|
25
|
+
* NARROWNESS (see index.ts). This file adds runtime values — two frozen name
|
|
26
|
+
* arrays and one error-code-to-phase map. That is the same shape as
|
|
27
|
+
* `RUN_ERROR_CODES`, which this package already publishes. It carries no
|
|
28
|
+
* engine, no endpoint, no credential, and no backend logic. Event names
|
|
29
|
+
* describe which buttons exist, which a contractor installing the package can
|
|
30
|
+
* already observe by using the product.
|
|
31
|
+
*/
|
|
32
|
+
import { RUN_ERROR_CODES } from './run-errors.js';
|
|
33
|
+
/**
|
|
34
|
+
* Schema version stamped on every event emitted by a contract-aware wrapper.
|
|
35
|
+
*
|
|
36
|
+
* Version 1 is everything emitted before the GA cutover: mixed identity,
|
|
37
|
+
* success events that fired before the operation succeeded, camelCase and
|
|
38
|
+
* snake_case properties side by side. Version 2 is this contract.
|
|
39
|
+
*
|
|
40
|
+
* The versions are NOT semantically comparable and must never be summed in one
|
|
41
|
+
* tile. Identity is preserved across the cutover by aliasing, so a person's
|
|
42
|
+
* history survives; the meaning of their events does not.
|
|
43
|
+
*/
|
|
44
|
+
export const ANALYTICS_SCHEMA_VERSION = 2;
|
|
45
|
+
/**
|
|
46
|
+
* Every run error code, mapped to the phase it belongs to.
|
|
47
|
+
*
|
|
48
|
+
* EXHAUSTIVE BY CONSTRUCTION: the `Record<RunErrorCode, …>` type means adding
|
|
49
|
+
* a code to the taxonomy without assigning it a phase is a compile error in
|
|
50
|
+
* this package, which is a merge gate in both repositories. That is deliberate
|
|
51
|
+
* — the alternative is a default branch, and a default branch silently files
|
|
52
|
+
* every new code under `unknown`.
|
|
53
|
+
*/
|
|
54
|
+
export const ERROR_CODE_PHASE = {
|
|
55
|
+
INVALID_REFERENCE_URL: 'input_validation',
|
|
56
|
+
INVALID_TARGET_URL: 'input_validation',
|
|
57
|
+
TARGET_AUTH_WALL: 'target_capture',
|
|
58
|
+
TARGET_UNREACHABLE: 'target_capture',
|
|
59
|
+
TARGET_TIMEOUT: 'target_capture',
|
|
60
|
+
TARGET_CSP_BLOCKED: 'target_capture',
|
|
61
|
+
SESSION_EXPIRED: 'target_capture',
|
|
62
|
+
CAPABILITY_UNAVAILABLE: 'target_capture',
|
|
63
|
+
PROVIDER_REQUEST_REJECTED: 'target_capture',
|
|
64
|
+
FIGMA_ACCESS_DENIED: 'figma_fetch',
|
|
65
|
+
FIGMA_TOKEN_EXPIRED: 'figma_fetch',
|
|
66
|
+
FIGMA_NOT_FOUND: 'figma_fetch',
|
|
67
|
+
FIGMA_RATE_LIMITED: 'figma_fetch',
|
|
68
|
+
DESIGN_SYSTEM_NOT_AVAILABLE: 'design_system_resolve',
|
|
69
|
+
DESIGN_SYSTEM_CONTEXT_NOT_AVAILABLE: 'design_system_resolve',
|
|
70
|
+
DESIGN_SYSTEM_INCOMPATIBLE: 'design_system_resolve',
|
|
71
|
+
PIPELINE_TIMEOUT: 'pipeline',
|
|
72
|
+
PIPELINE_ERROR: 'pipeline',
|
|
73
|
+
ZERO_ELEMENTS_MATCHED: 'pipeline',
|
|
74
|
+
FLOW_STEP_FAILED: 'pipeline',
|
|
75
|
+
PERSIST_FAILED: 'persist',
|
|
76
|
+
UNKNOWN_ERROR: 'unknown',
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Phase for a code, tolerating values from outside the taxonomy.
|
|
80
|
+
*
|
|
81
|
+
* The admin dashboard reads `error_code` out of Postgres, where it is a bare
|
|
82
|
+
* TEXT column with no FK to the taxonomy. A row written by an older deploy can
|
|
83
|
+
* therefore hold a code this build has never heard of. Returning `'unknown'`
|
|
84
|
+
* for that is right; throwing would take down the tile.
|
|
85
|
+
*/
|
|
86
|
+
export function errorPhaseFor(code) {
|
|
87
|
+
if (!code)
|
|
88
|
+
return 'unknown';
|
|
89
|
+
return ERROR_CODE_PHASE[code] ?? 'unknown';
|
|
90
|
+
}
|
|
91
|
+
/** True when `code` is a member of the current taxonomy. */
|
|
92
|
+
export function isKnownErrorCode(code) {
|
|
93
|
+
return !!code && RUN_ERROR_CODES.includes(code);
|
|
94
|
+
}
|
|
95
|
+
// ── Event names ──────────────────────────────────────────────────────────────
|
|
96
|
+
//
|
|
97
|
+
// TWO LISTS, ONE UNION.
|
|
98
|
+
//
|
|
99
|
+
// GA events are the ones the GA Command Center is built on. They have typed,
|
|
100
|
+
// required properties and their firing conditions are specified in the plan.
|
|
101
|
+
//
|
|
102
|
+
// Secondary events already exist in the extension and keep flowing. They are
|
|
103
|
+
// listed because the drift gate is exhaustive: an emitted name absent from
|
|
104
|
+
// this file fails CI. Listing them is not an endorsement — several are
|
|
105
|
+
// diagnostic and must stay out of GA tiles.
|
|
106
|
+
//
|
|
107
|
+
// The extension's raw names differ from the webapp's on purpose. Renaming them
|
|
108
|
+
// would orphan existing history for no gain; PostHog *actions* combine the
|
|
109
|
+
// pairs (see plan §13). What matters is that both are declared here.
|
|
110
|
+
/** Events the GA dashboard is built on. Typed properties, specified triggers. */
|
|
111
|
+
export const GA_EVENT_NAMES = [
|
|
112
|
+
// webapp
|
|
113
|
+
'signup_started',
|
|
114
|
+
'signup_completed',
|
|
115
|
+
'onboarding_started',
|
|
116
|
+
'onboarding_completed',
|
|
117
|
+
'figma_connected',
|
|
118
|
+
'environment_connected',
|
|
119
|
+
'web_validation_started',
|
|
120
|
+
'web_validation_completed',
|
|
121
|
+
'web_validation_failed',
|
|
122
|
+
'web_validation_cancelled',
|
|
123
|
+
'report_viewed',
|
|
124
|
+
'report_shared',
|
|
125
|
+
'diff_mark_resolved',
|
|
126
|
+
'upgrade_clicked',
|
|
127
|
+
'checkout_started',
|
|
128
|
+
'feedback_submitted',
|
|
129
|
+
// extension — existing names, corrected semantics
|
|
130
|
+
'extension_opened',
|
|
131
|
+
'validation_start',
|
|
132
|
+
'validation_accepted',
|
|
133
|
+
'validation_complete',
|
|
134
|
+
'validation_error',
|
|
135
|
+
'validation_cancel',
|
|
136
|
+
'review_saved',
|
|
137
|
+
'figma_auth_success',
|
|
138
|
+
'figma_auth_error',
|
|
139
|
+
'extension_share_copied',
|
|
140
|
+
'upgrade_modal_viewed',
|
|
141
|
+
'upgrade_cta_clicked',
|
|
142
|
+
];
|
|
143
|
+
/**
|
|
144
|
+
* Events that exist but are NOT GA metrics.
|
|
145
|
+
*
|
|
146
|
+
* `checkout_completed` is here rather than in `GA_EVENT_NAMES` deliberately.
|
|
147
|
+
* It fires in the browser on return from Stripe, which is neither reliable
|
|
148
|
+
* (the tab can be closed) nor authoritative (the webhook is). Paid conversion
|
|
149
|
+
* comes from `teams.subscription_status` and Stripe. This event is a funnel
|
|
150
|
+
* diagnostic for "did the user come back", nothing more, and must be labelled
|
|
151
|
+
* as such wherever it appears.
|
|
152
|
+
*/
|
|
153
|
+
export const SECONDARY_EVENT_NAMES = [
|
|
154
|
+
'checkout_completed',
|
|
155
|
+
// extension UI interaction + diagnostics
|
|
156
|
+
'console_errors',
|
|
157
|
+
'diff_expanded',
|
|
158
|
+
'drawer_filter_changed',
|
|
159
|
+
'review_save_error',
|
|
160
|
+
'review_verdict',
|
|
161
|
+
'review_verdict_all',
|
|
162
|
+
'screenshot_exported',
|
|
163
|
+
'ui_crash',
|
|
164
|
+
'validation_resume',
|
|
165
|
+
'validation_retry',
|
|
166
|
+
'view_changed',
|
|
167
|
+
// extension pipeline internals, emitted from static IIFEs (hp.js)
|
|
168
|
+
'auth_refresh',
|
|
169
|
+
'consensus_complete',
|
|
170
|
+
'consensus_error',
|
|
171
|
+
'extension_validation_failed',
|
|
172
|
+
'idempotency_dedup_client',
|
|
173
|
+
'pipeline_complete',
|
|
174
|
+
'pipeline_phase',
|
|
175
|
+
'pipeline_resume',
|
|
176
|
+
'rate_limit_hit',
|
|
177
|
+
'upgrade_wall_hit',
|
|
178
|
+
];
|
|
179
|
+
/** Every event name any Fidel client may emit. */
|
|
180
|
+
export const ANALYTICS_EVENT_NAMES = [
|
|
181
|
+
...GA_EVENT_NAMES,
|
|
182
|
+
...SECONDARY_EVENT_NAMES,
|
|
183
|
+
];
|
|
184
|
+
// ── Redaction ────────────────────────────────────────────────────────────────
|
|
185
|
+
/**
|
|
186
|
+
* Property names that must never reach an analytics backend, at any value.
|
|
187
|
+
*
|
|
188
|
+
* This is a NAME denylist and therefore a backstop, not the control. The
|
|
189
|
+
* control is that call sites do not pass these. It exists because the cost of
|
|
190
|
+
* one leaked token is unbounded and the cost of a `Set.has()` is not.
|
|
191
|
+
*
|
|
192
|
+
* Design-token names and CSS custom-property names are NOT credentials and are
|
|
193
|
+
* deliberately absent from this list — they are frequently the only useful
|
|
194
|
+
* debugging signal on a DS run.
|
|
195
|
+
*/
|
|
196
|
+
export const FORBIDDEN_PROPERTY_KEYS = [
|
|
197
|
+
'access_token',
|
|
198
|
+
'accessToken',
|
|
199
|
+
'refresh_token',
|
|
200
|
+
'refreshToken',
|
|
201
|
+
'authorization',
|
|
202
|
+
'Authorization',
|
|
203
|
+
'apikey',
|
|
204
|
+
'apiKey',
|
|
205
|
+
'api_key',
|
|
206
|
+
'anon_key',
|
|
207
|
+
'anonKey',
|
|
208
|
+
'cookie',
|
|
209
|
+
'Cookie',
|
|
210
|
+
'share_token',
|
|
211
|
+
'shareToken',
|
|
212
|
+
'figma_access_token',
|
|
213
|
+
'figma_token',
|
|
214
|
+
'figmaToken',
|
|
215
|
+
'pat',
|
|
216
|
+
'password',
|
|
217
|
+
'jwt',
|
|
218
|
+
'secret',
|
|
219
|
+
't', // the `?t=` share-token query parameter
|
|
220
|
+
];
|
|
221
|
+
/**
|
|
222
|
+
* Strip forbidden keys and `undefined` values from a property bag.
|
|
223
|
+
*
|
|
224
|
+
* `undefined` is dropped rather than sent because PostHog stores it as a real
|
|
225
|
+
* property value, which then shows up in the UI as an existing-but-empty
|
|
226
|
+
* property and makes "is not set" filters wrong.
|
|
227
|
+
*/
|
|
228
|
+
export function sanitizeProperties(props) {
|
|
229
|
+
if (!props)
|
|
230
|
+
return {};
|
|
231
|
+
const forbidden = new Set(FORBIDDEN_PROPERTY_KEYS.map((k) => k.toLowerCase()));
|
|
232
|
+
const out = {};
|
|
233
|
+
for (const [key, value] of Object.entries(props)) {
|
|
234
|
+
if (value === undefined)
|
|
235
|
+
continue;
|
|
236
|
+
if (forbidden.has(key.toLowerCase()))
|
|
237
|
+
continue;
|
|
238
|
+
out[key] = value;
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { RUN_ERROR_CODES, ERROR_CODE_META, mapSnapshotErrorToCode, mapFigmaErrorToCode, resolveRunDisplay, } from './run-errors.js';
|
|
2
2
|
export type { RunErrorCode, RunErrorMeta, ErrorMapping, RunDisplay, } from './run-errors.js';
|
|
3
3
|
export type { IntakeFramework, UnsupportedStack, UnsupportedReason, UnresolvedReferenceSection, UnresolvedReferenceReason, UnresolvedReference, ParsedThemeColor, ParsedThemeTypography, ParsedThemeSize, ShadcnConfig, ParsedTheme, IntakeDetection, TokenSourceKind, TokenSource, PackageRole, PackageInfo, } from './theme-intake.js';
|
|
4
|
+
export { ANALYTICS_SCHEMA_VERSION, ANALYTICS_EVENT_NAMES, GA_EVENT_NAMES, SECONDARY_EVENT_NAMES, ERROR_CODE_PHASE, FORBIDDEN_PROPERTY_KEYS, errorPhaseFor, isKnownErrorCode, sanitizeProperties, } from './analytics.js';
|
|
5
|
+
export type { AnalyticsSurface, AnalyticsEnvironment, AnalyticsContext, AnalyticsPersonProperties, AnalyticsEventName, GaEventName, SecondaryEventName, AnalyticsEventProperties, AnalyticsPayload, ValidationMode, ValidationErrorPhase, ResultCompleteness, ValidationEventProperties, ValidationStartedProperties, WebValidationCompletedProperties, ValidationFailedProperties, ValidationCancelledProperties, ReportViewedProperties, } from './analytics.js';
|
package/dist/index.js
CHANGED
|
@@ -16,3 +16,21 @@
|
|
|
16
16
|
// Adding an export here is an architectural change, not a convenience — it needs
|
|
17
17
|
// the same review as any cross-surface contract.
|
|
18
18
|
export { RUN_ERROR_CODES, ERROR_CODE_META, mapSnapshotErrorToCode, mapFigmaErrorToCode, resolveRunDisplay, } from './run-errors.js';
|
|
19
|
+
// analytics — client event names and property shapes (GA Phase 1).
|
|
20
|
+
//
|
|
21
|
+
// WHY THIS BELONGS HERE, against the narrowness rule above: it is the only
|
|
22
|
+
// mechanism that spans both repositories. The webapp and extension each held a
|
|
23
|
+
// local, self-declared "authoritative" identity and event definition; they
|
|
24
|
+
// disagreed, nothing failed, and retention cohorts were silently wrong for
|
|
25
|
+
// months. A gate that lives in one repo cannot catch that.
|
|
26
|
+
//
|
|
27
|
+
// It carries two frozen string arrays, one error-code-to-phase map and one
|
|
28
|
+
// pure sanitizer — the same runtime shape as RUN_ERROR_CODES above. No engine,
|
|
29
|
+
// no endpoint, no credential, no backend logic. Event names describe which
|
|
30
|
+
// buttons exist, which anyone using the product can already observe.
|
|
31
|
+
//
|
|
32
|
+
// What it deliberately does NOT carry are the SDK wrappers: initialization,
|
|
33
|
+
// identity resolution, storage and environment detection stay per-surface,
|
|
34
|
+
// because a shared module would have to branch on surface and a shared module
|
|
35
|
+
// full of surface branches is how the divergence started.
|
|
36
|
+
export { ANALYTICS_SCHEMA_VERSION, ANALYTICS_EVENT_NAMES, GA_EVENT_NAMES, SECONDARY_EVENT_NAMES, ERROR_CODE_PHASE, FORBIDDEN_PROPERTY_KEYS, errorPhaseFor, isKnownErrorCode, sanitizeProperties, } from './analytics.js';
|
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.
|
|
10
|
+
"version": "0.7.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,
|
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
"./onboarding-config": {
|
|
32
32
|
"types": "./dist/onboarding-config.d.ts",
|
|
33
33
|
"import": "./dist/onboarding-config.js"
|
|
34
|
+
},
|
|
35
|
+
"./analytics": {
|
|
36
|
+
"types": "./dist/analytics.d.ts",
|
|
37
|
+
"import": "./dist/analytics.js"
|
|
34
38
|
}
|
|
35
39
|
},
|
|
36
40
|
"files": [
|