@cross-deck/web 0.6.0 → 0.10.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/index.d.mts CHANGED
@@ -139,6 +139,23 @@ interface CrossdeckOptions {
139
139
  * `Crossdeck.setDebugMode(true)` after init.
140
140
  */
141
141
  debug?: boolean;
142
+ /**
143
+ * Respect the browser's Do Not Track signal at init (v0.10.0+).
144
+ * Default `false`. When `true` AND the user has `navigator.doNotTrack === "1"`,
145
+ * the SDK boots with analytics / marketing / errors all denied —
146
+ * locked off even if the developer later calls `Crossdeck.consent({...})`.
147
+ * Industry has effectively deprecated DNT, but opt-in support is the
148
+ * polite default for privacy-first apps.
149
+ */
150
+ respectDnt?: boolean;
151
+ /**
152
+ * Scrub PII-shaped strings (email addresses, card numbers) from
153
+ * URL paths, event properties, and acquisition referrer before they
154
+ * leave the SDK. Default `true` — Stripe-grade. Disable only if your
155
+ * pipeline does its own PII redaction downstream and you need the
156
+ * raw strings.
157
+ */
158
+ scrubPii?: boolean;
142
159
  }
143
160
  /** Auto-tracking flags. See CrossdeckOptions.autoTrack. */
144
161
  interface AutoTrackOptions {
@@ -148,6 +165,23 @@ interface AutoTrackOptions {
148
165
  pageViews: boolean;
149
166
  /** Auto-attach os/browser/locale/screen/etc to every event's `properties`. Default true (browser only). */
150
167
  deviceInfo: boolean;
168
+ /**
169
+ * Click autocapture — fire `element.clicked` for every interactive
170
+ * click on the page. Default true. Mixpanel/Amplitude pattern. Powers
171
+ * Crossdeck's funnel-attribution USP ("clicked X then converted").
172
+ * Privacy: skips form inputs / password fields / [class~="cd-noTrack"]
173
+ * subtrees. Override on individual elements with data-cd-event="custom"
174
+ * or data-cd-prop-* for custom property tagging.
175
+ */
176
+ clicks: boolean;
177
+ /**
178
+ * Web Vitals capture (v0.9.0+) — emits `webvitals.lcp`, `webvitals.inp`,
179
+ * `webvitals.cls`, `webvitals.fcp`, `webvitals.ttfb` events using the
180
+ * browser's `PerformanceObserver`. Defaults to true in browsers,
181
+ * no-op everywhere else. Disable if you have a separate RUM provider
182
+ * (DataDog, Sentry Performance) and don't want duplicates.
183
+ */
184
+ webVitals: boolean;
151
185
  }
152
186
  /** Minimal interface for any pluggable key-value persistence. */
153
187
  interface KeyValueStorage {
@@ -155,10 +189,44 @@ interface KeyValueStorage {
155
189
  setItem(key: string, value: string): void;
156
190
  removeItem(key: string): void;
157
191
  }
158
- /** Identity hint object passed to identify() — at least one field required. */
192
+ /**
193
+ * Identity hint + profile traits passed to identify().
194
+ *
195
+ * `traits` is a free-form bag of profile data (name, plan, signupDate,
196
+ * teamRole, etc.) that gets persisted on the Crossdeck customer record
197
+ * and attached to every subsequent event of the identified user as
198
+ * `$user.<key>` properties for dashboard filtering.
199
+ *
200
+ * Like event properties, traits are validated at the SDK boundary —
201
+ * functions/symbols/undefined dropped, Date / BigInt / Error coerced,
202
+ * strings > 1024 chars truncated. Caller's object is never mutated.
203
+ */
159
204
  interface IdentifyOptions {
160
205
  /** Optional email to attach to the customer record. */
161
206
  email?: string;
207
+ /**
208
+ * Optional profile traits. Examples:
209
+ * `{ name: "Wes", plan: "pro", signedUpAt: "2026-05-11" }`
210
+ *
211
+ * Treated like event properties — values are sanitised at the SDK
212
+ * boundary so a `{ avatar: <File>, callback: () => {} }` payload
213
+ * doesn't crash the alias request. Server-side, traits land on
214
+ * `customers/{cdcust}.traits` (additively — existing fields are
215
+ * preserved unless the new identify call overrides them).
216
+ */
217
+ traits?: Record<string, unknown>;
218
+ }
219
+ /**
220
+ * Group context — Mixpanel-style. Identifies a customer's membership
221
+ * in an organisational entity (org, account, team, workspace) so B2B
222
+ * dashboards can answer "how is account X using my product".
223
+ *
224
+ * Attached to every event as `$groups.<type>` until cleared via
225
+ * `Crossdeck.group(type, null)`. Multiple types can coexist (e.g.
226
+ * `org` + `team`) — the SDK keeps a map keyed by type.
227
+ */
228
+ interface GroupTraits {
229
+ [key: string]: unknown;
162
230
  }
163
231
  /** Properties payload for track(). Arbitrary key/value, JSON-serialisable, ≤ 8 KB. */
164
232
  type EventProperties = Record<string, unknown>;
@@ -175,9 +243,34 @@ interface Diagnostics {
175
243
  developerUserId: string | null;
176
244
  sdkVersion: string | null;
177
245
  baseUrl: string | null;
246
+ /**
247
+ * Last `serverTime` value the SDK saw on a /sdk/heartbeat response,
248
+ * along with the local clock value AT that moment. Lets dashboards
249
+ * (and the developer, in debug mode) detect a wrong-system-clock
250
+ * problem before it corrupts a day of analytics. Null until the
251
+ * first heartbeat completes.
252
+ */
253
+ clock: {
254
+ /** Server's view of "now" from the last heartbeat (epoch ms). */
255
+ lastServerTime: number | null;
256
+ /** Client's `Date.now()` taken at the same moment as `lastServerTime`. */
257
+ lastClientTime: number | null;
258
+ /**
259
+ * `lastClientTime - lastServerTime` — positive means the client
260
+ * clock is AHEAD of the server. Outside ±5 minutes is suspicious
261
+ * and worth surfacing to the developer.
262
+ */
263
+ skewMs: number | null;
264
+ };
178
265
  entitlements: {
179
266
  count: number;
180
267
  lastUpdated: number;
268
+ /**
269
+ * Cumulative count of listener invocations that threw. Swallowed
270
+ * inside the cache (a buggy consumer must not crash the SDK) but
271
+ * surfaced here so developers can spot broken subscribers.
272
+ */
273
+ listenerErrors: number;
181
274
  };
182
275
  events: {
183
276
  buffered: number;
@@ -185,6 +278,13 @@ interface Diagnostics {
185
278
  inFlight: number;
186
279
  lastFlushAt: number;
187
280
  lastError: string | null;
281
+ /** Consecutive flush failures since the last success. */
282
+ consecutiveFailures: number;
283
+ /**
284
+ * When the next retry is scheduled (epoch ms), or null if the queue
285
+ * is idle / healthy.
286
+ */
287
+ nextRetryAt: number | null;
188
288
  };
189
289
  }
190
290
 
@@ -230,6 +330,44 @@ interface Diagnostics {
230
330
 
231
331
  type EntitlementsListener = (entitlements: PublicEntitlement[]) => void;
232
332
 
333
+ /**
334
+ * Consent gating — GDPR / CCPA-grade kill switches.
335
+ *
336
+ * Three independent dimensions, each defaulting to "granted" but
337
+ * runtime-overridable:
338
+ *
339
+ * analytics — track(), identify(), heartbeat(), session/page auto-
340
+ * emissions. Off → events drop silently, no network
341
+ * calls fire.
342
+ * marketing — paid-traffic click IDs (gclid/fbclid/etc) and
343
+ * acquisition referrer URL. Off → these get scrubbed
344
+ * before they ever land in the event bag.
345
+ * errors — error / breadcrumb / Web Vitals capture. Off → no
346
+ * webvitals.* events emitted, no error reporting (when
347
+ * Phase 3 errors land).
348
+ *
349
+ * Why this granularity: real consent banners offer "Analytics",
350
+ * "Marketing", "Functional" as separate boxes. The SDK has to match.
351
+ *
352
+ * Default state: every dimension is granted. The developer must
353
+ * explicitly call `Crossdeck.consent({ analytics: false })` before
354
+ * the first event to opt OUT — same convention as Google Tag Manager
355
+ * Consent Mode. To start in deny mode, call `init(...)` then
356
+ * immediately `consent({ analytics: false, marketing: false, errors:
357
+ * false })` before any user activity.
358
+ *
359
+ * DNT (Do Not Track) browser header is checked once at init and
360
+ * applied as an automatic deny across all dimensions when
361
+ * `respectDnt: true` is set in CrossdeckOptions (default false because
362
+ * the industry has effectively deprecated DNT — but opt-in support
363
+ * is the polite default for privacy-first apps).
364
+ */
365
+ interface ConsentState {
366
+ analytics: boolean;
367
+ marketing: boolean;
368
+ errors: boolean;
369
+ }
370
+
233
371
  /**
234
372
  * Public API surface for @cross-deck/web.
235
373
  *
@@ -285,8 +423,88 @@ declare class CrossdeckClient {
285
423
  /**
286
424
  * Link the anonymous device to a developer-supplied user ID. Cache
287
425
  * the resolved Crossdeck customer for follow-up calls.
426
+ *
427
+ * v0.9.0+ accepts an optional `traits` bag — profile data (name,
428
+ * plan, signupDate, role) persisted on the Crossdeck customer record
429
+ * and queryable from dashboards. Traits are sanitised through the
430
+ * same validator that gates `track()` properties, so a `{ avatar:
431
+ * <File>, onSave: () => {} }` payload can't corrupt the alias call.
432
+ *
433
+ * Crossdeck.identify("user_847", {
434
+ * email: "wes@pinet.co.za",
435
+ * traits: { name: "Wes", plan: "pro", signedUpAt: "2026-05-11" },
436
+ * });
288
437
  */
289
- identify(userId: string, _options?: IdentifyOptions): Promise<AliasResult>;
438
+ identify(userId: string, options?: IdentifyOptions): Promise<AliasResult>;
439
+ /**
440
+ * Register super-properties — Mixpanel pattern. Once set, every
441
+ * subsequent event of THIS SDK instance carries these keys on its
442
+ * properties bag automatically.
443
+ *
444
+ * Crossdeck.register({ plan: "pro", releaseChannel: "beta" });
445
+ * Crossdeck.track("paywall_shown"); // includes plan + releaseChannel
446
+ *
447
+ * Values that are `null` are deleted (the explicit "stop tracking
448
+ * this key" idiom). Returns the resulting bag.
449
+ *
450
+ * Sanitised through `validateEventProperties` so a `{ avatar: File }`
451
+ * payload can't poison the queue at flush time.
452
+ */
453
+ register(properties: Record<string, unknown>): Record<string, unknown>;
454
+ /** Remove a single super-property key. Idempotent. */
455
+ unregister(key: string): void;
456
+ /** Snapshot of the current super-property bag. */
457
+ getSuperProperties(): Record<string, unknown>;
458
+ /**
459
+ * Associate the current user with a group (org, team, account, etc.).
460
+ * Mixpanel / Segment "Group Analytics" pattern.
461
+ *
462
+ * Crossdeck.group("org", "acme_inc");
463
+ * Crossdeck.group("team", "design", { headcount: 12 });
464
+ *
465
+ * Once set, every subsequent event carries `$groups.<type>: id` on
466
+ * its properties bag, enabling B2B dashboards ("how is Acme using
467
+ * the product"). Pass `id: null` to clear a group membership.
468
+ */
469
+ group(type: string, id: string | null, traits?: GroupTraits): void;
470
+ /** Snapshot of the current groups map keyed by type. */
471
+ getGroups(): Record<string, {
472
+ id: string;
473
+ traits?: Record<string, unknown>;
474
+ }>;
475
+ /**
476
+ * Update consent state. Three independent dimensions:
477
+ *
478
+ * analytics — track() + identify() + auto-emissions
479
+ * marketing — paid-traffic click IDs + referrer URL on events
480
+ * errors — Web Vitals + (future) error reporting
481
+ *
482
+ * Each defaults to `true` (granted). Pass partial state — only the
483
+ * keys you provide are changed.
484
+ *
485
+ * Crossdeck.consent({ analytics: false });
486
+ * Crossdeck.consent({ marketing: true, errors: true });
487
+ *
488
+ * DNT-derived denies cannot be flipped back on; if the browser said
489
+ * "don't track" we don't track even if the developer code disagrees.
490
+ */
491
+ consent(state: Partial<ConsentState>): ConsentState;
492
+ /** Snapshot of the current consent state. */
493
+ consentStatus(): ConsentState;
494
+ /**
495
+ * GDPR/CCPA "right to be forgotten" — calls the backend's
496
+ * /v1/identity/forget endpoint to schedule a server-side deletion of
497
+ * the customer's events and profile, then wipes all local state
498
+ * (identity, entitlements, queue, super-props, persistent stores).
499
+ *
500
+ * Idempotent. Safe to call when no identity has been established
501
+ * (it just wipes the empty local state).
502
+ *
503
+ * After forget() resolves, the SDK is in the same shape as if the
504
+ * developer had called `Crossdeck.reset()` — a fresh anonymousId is
505
+ * minted and the next session is a brand new identity-graph entry.
506
+ */
507
+ forget(): Promise<void>;
290
508
  /**
291
509
  * Read the current customer's active entitlements from the server.
292
510
  * Updates the local cache so subsequent isEntitled() calls answer
@@ -402,7 +620,20 @@ declare class CrossdeckClient {
402
620
  * — matches the resolveCrossdeckCustomerId precedence on the server.
403
621
  */
404
622
  private identityQueryParams;
405
- /** Pick the right identity hint to embed on a queued event. */
623
+ /**
624
+ * Embed every known identity axis on the event. Earlier this returned
625
+ * just the highest-priority hint (cdcust → developerUserId → anonymousId)
626
+ * to keep payloads small, but that leaked into analytics: once a user
627
+ * was logged in, every subsequent page.viewed shipped without
628
+ * anonymousId, and `uniqExact(anonymous_id)` on the warehouse side
629
+ * counted 0 visitors for the entire authenticated app.
630
+ *
631
+ * Bank-grade rule: the server is the single source of truth on
632
+ * dedup. Send everything we know; let CH count by whichever axis
633
+ * matches the question. Each field is at most 32 bytes — sending
634
+ * three on every event costs ~80 bytes per request, which is
635
+ * trivial compared to the analytics correctness it buys.
636
+ */
406
637
  private identityHintForEvent;
407
638
  private mintEventId;
408
639
  }
@@ -436,12 +667,21 @@ interface CrossdeckErrorPayload {
436
667
  requestId?: string;
437
668
  /** HTTP status code if the error came from an API response. */
438
669
  status?: number;
670
+ /**
671
+ * Server-suggested wait (in milliseconds) before retrying. Populated
672
+ * from the `Retry-After` response header on 429 / 503. The header
673
+ * spec allows either delta-seconds or an HTTP-date; the parser below
674
+ * normalises both to milliseconds. Consumers MUST honour this — the
675
+ * server is telling you the safe rate.
676
+ */
677
+ retryAfterMs?: number;
439
678
  }
440
679
  declare class CrossdeckError extends Error {
441
680
  readonly type: CrossdeckErrorType;
442
681
  readonly code: string;
443
682
  readonly requestId?: string;
444
683
  readonly status?: number;
684
+ readonly retryAfterMs?: number;
445
685
  constructor(payload: CrossdeckErrorPayload);
446
686
  }
447
687
 
@@ -504,9 +744,45 @@ declare class MemoryStorage implements KeyValueStorage {
504
744
  * fetch shim, no transitive deps.
505
745
  */
506
746
  declare const SDK_NAME = "@cross-deck/web";
507
- declare const SDK_VERSION = "0.6.0";
747
+ declare const SDK_VERSION = "0.10.0";
508
748
  declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
509
749
 
750
+ /**
751
+ * Machine-readable index of every error code the SDK can throw, with
752
+ * a short description and a hint on what action to take. Published
753
+ * verbatim as `crossdeck-error-codes.json` in the npm tarball so AI
754
+ * integration assistants, error-aggregator dashboards (Sentry,
755
+ * DataDog), and the Crossdeck dashboard can render human-friendly
756
+ * messages without parsing freeform `message` strings.
757
+ *
758
+ * Stripe publishes the same surface at stripe.com/docs/error-codes;
759
+ * developers love it because every code has a canonical "what does
760
+ * this mean / what should I do" answer.
761
+ *
762
+ * Adding a new error code:
763
+ * 1. Add the code string to the union in `errors.ts` (where used).
764
+ * 2. Add an entry here.
765
+ * 3. The next `npm run build` regenerates the JSON sidecar.
766
+ *
767
+ * Keep entries terse — the consumer surfaces this in tooltips and
768
+ * automated tickets, not in long-form docs.
769
+ */
770
+ interface ErrorCodeEntry {
771
+ /** The string thrown as CrossdeckError.code. */
772
+ code: string;
773
+ /** CrossdeckError.type — broad category. */
774
+ type: "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "internal_error" | "network_error" | "configuration_error";
775
+ /** One-sentence description. Surfaced verbatim in dashboards. */
776
+ description: string;
777
+ /** What the developer should do. Imperative phrasing. */
778
+ resolution: string;
779
+ /** True for codes the SDK can auto-recover from (no developer action). */
780
+ retryable: boolean;
781
+ }
782
+ declare const CROSSDECK_ERROR_CODES: readonly ErrorCodeEntry[];
783
+ /** Lookup helper — returns the entry matching a CrossdeckError.code, or undefined. */
784
+ declare function getErrorCode(code: string): ErrorCodeEntry | undefined;
785
+
510
786
  /**
511
787
  * Device + environment enrichment.
512
788
  *
@@ -541,4 +817,4 @@ interface DeviceInfo {
541
817
  appVersion?: string;
542
818
  }
543
819
 
544
- export { type AliasResult, type AuditRail, type AutoTrackOptions, Crossdeck, CrossdeckClient, CrossdeckError, type CrossdeckErrorPayload, type CrossdeckErrorType, type CrossdeckOptions, DEFAULT_BASE_URL, type DeviceInfo, type Diagnostics, type EntitlementsListResponse, type Environment, type EventProperties, type HeartbeatResponse, type IdentifyOptions, type KeyValueStorage, MemoryStorage, type Platform, type PublicEntitlement, type PurchaseResult, SDK_NAME, SDK_VERSION };
820
+ export { type AliasResult, type AuditRail, type AutoTrackOptions, CROSSDECK_ERROR_CODES, type ConsentState, Crossdeck, CrossdeckClient, CrossdeckError, type CrossdeckErrorPayload, type CrossdeckErrorType, type CrossdeckOptions, DEFAULT_BASE_URL, type DeviceInfo, type Diagnostics, type EntitlementsListResponse, type Environment, type ErrorCodeEntry, type EventProperties, type GroupTraits, type HeartbeatResponse, type IdentifyOptions, type KeyValueStorage, MemoryStorage, type Platform, type PublicEntitlement, type PurchaseResult, SDK_NAME, SDK_VERSION, getErrorCode };