@cross-deck/web 0.7.0 → 1.0.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.ts 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 {
@@ -157,6 +174,25 @@ interface AutoTrackOptions {
157
174
  * or data-cd-prop-* for custom property tagging.
158
175
  */
159
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;
185
+ /**
186
+ * Error capture (v1.0.0+) — installs window.onerror +
187
+ * window.onunhandledrejection listeners, wraps fetch + XHR to catch
188
+ * 5xx + network failures, ships each captured error as a Crossdeck
189
+ * event (kind: error.unhandled / error.unhandledrejection /
190
+ * error.handled / error.http / error.message). Errors gate on
191
+ * `consent.errors`. Rate-limited per-fingerprint so a runaway loop
192
+ * can't flood the queue; browser-extension noise filtered by
193
+ * default. Default true in browsers, no-op everywhere else.
194
+ */
195
+ errors: boolean;
160
196
  }
161
197
  /** Minimal interface for any pluggable key-value persistence. */
162
198
  interface KeyValueStorage {
@@ -164,10 +200,44 @@ interface KeyValueStorage {
164
200
  setItem(key: string, value: string): void;
165
201
  removeItem(key: string): void;
166
202
  }
167
- /** Identity hint object passed to identify() — at least one field required. */
203
+ /**
204
+ * Identity hint + profile traits passed to identify().
205
+ *
206
+ * `traits` is a free-form bag of profile data (name, plan, signupDate,
207
+ * teamRole, etc.) that gets persisted on the Crossdeck customer record
208
+ * and attached to every subsequent event of the identified user as
209
+ * `$user.<key>` properties for dashboard filtering.
210
+ *
211
+ * Like event properties, traits are validated at the SDK boundary —
212
+ * functions/symbols/undefined dropped, Date / BigInt / Error coerced,
213
+ * strings > 1024 chars truncated. Caller's object is never mutated.
214
+ */
168
215
  interface IdentifyOptions {
169
216
  /** Optional email to attach to the customer record. */
170
217
  email?: string;
218
+ /**
219
+ * Optional profile traits. Examples:
220
+ * `{ name: "Wes", plan: "pro", signedUpAt: "2026-05-11" }`
221
+ *
222
+ * Treated like event properties — values are sanitised at the SDK
223
+ * boundary so a `{ avatar: <File>, callback: () => {} }` payload
224
+ * doesn't crash the alias request. Server-side, traits land on
225
+ * `customers/{cdcust}.traits` (additively — existing fields are
226
+ * preserved unless the new identify call overrides them).
227
+ */
228
+ traits?: Record<string, unknown>;
229
+ }
230
+ /**
231
+ * Group context — Mixpanel-style. Identifies a customer's membership
232
+ * in an organisational entity (org, account, team, workspace) so B2B
233
+ * dashboards can answer "how is account X using my product".
234
+ *
235
+ * Attached to every event as `$groups.<type>` until cleared via
236
+ * `Crossdeck.group(type, null)`. Multiple types can coexist (e.g.
237
+ * `org` + `team`) — the SDK keeps a map keyed by type.
238
+ */
239
+ interface GroupTraits {
240
+ [key: string]: unknown;
171
241
  }
172
242
  /** Properties payload for track(). Arbitrary key/value, JSON-serialisable, ≤ 8 KB. */
173
243
  type EventProperties = Record<string, unknown>;
@@ -184,9 +254,34 @@ interface Diagnostics {
184
254
  developerUserId: string | null;
185
255
  sdkVersion: string | null;
186
256
  baseUrl: string | null;
257
+ /**
258
+ * Last `serverTime` value the SDK saw on a /sdk/heartbeat response,
259
+ * along with the local clock value AT that moment. Lets dashboards
260
+ * (and the developer, in debug mode) detect a wrong-system-clock
261
+ * problem before it corrupts a day of analytics. Null until the
262
+ * first heartbeat completes.
263
+ */
264
+ clock: {
265
+ /** Server's view of "now" from the last heartbeat (epoch ms). */
266
+ lastServerTime: number | null;
267
+ /** Client's `Date.now()` taken at the same moment as `lastServerTime`. */
268
+ lastClientTime: number | null;
269
+ /**
270
+ * `lastClientTime - lastServerTime` — positive means the client
271
+ * clock is AHEAD of the server. Outside ±5 minutes is suspicious
272
+ * and worth surfacing to the developer.
273
+ */
274
+ skewMs: number | null;
275
+ };
187
276
  entitlements: {
188
277
  count: number;
189
278
  lastUpdated: number;
279
+ /**
280
+ * Cumulative count of listener invocations that threw. Swallowed
281
+ * inside the cache (a buggy consumer must not crash the SDK) but
282
+ * surfaced here so developers can spot broken subscribers.
283
+ */
284
+ listenerErrors: number;
190
285
  };
191
286
  events: {
192
287
  buffered: number;
@@ -194,6 +289,13 @@ interface Diagnostics {
194
289
  inFlight: number;
195
290
  lastFlushAt: number;
196
291
  lastError: string | null;
292
+ /** Consecutive flush failures since the last success. */
293
+ consecutiveFailures: number;
294
+ /**
295
+ * When the next retry is scheduled (epoch ms), or null if the queue
296
+ * is idle / healthy.
297
+ */
298
+ nextRetryAt: number | null;
197
299
  };
198
300
  }
199
301
 
@@ -239,6 +341,187 @@ interface Diagnostics {
239
341
 
240
342
  type EntitlementsListener = (entitlements: PublicEntitlement[]) => void;
241
343
 
344
+ /**
345
+ * Consent gating — GDPR / CCPA-grade kill switches.
346
+ *
347
+ * Three independent dimensions, each defaulting to "granted" but
348
+ * runtime-overridable:
349
+ *
350
+ * analytics — track(), identify(), heartbeat(), session/page auto-
351
+ * emissions. Off → events drop silently, no network
352
+ * calls fire.
353
+ * marketing — paid-traffic click IDs (gclid/fbclid/etc) and
354
+ * acquisition referrer URL. Off → these get scrubbed
355
+ * before they ever land in the event bag.
356
+ * errors — error / breadcrumb / Web Vitals capture. Off → no
357
+ * webvitals.* events emitted, no error reporting (when
358
+ * Phase 3 errors land).
359
+ *
360
+ * Why this granularity: real consent banners offer "Analytics",
361
+ * "Marketing", "Functional" as separate boxes. The SDK has to match.
362
+ *
363
+ * Default state: every dimension is granted. The developer must
364
+ * explicitly call `Crossdeck.consent({ analytics: false })` before
365
+ * the first event to opt OUT — same convention as Google Tag Manager
366
+ * Consent Mode. To start in deny mode, call `init(...)` then
367
+ * immediately `consent({ analytics: false, marketing: false, errors:
368
+ * false })` before any user activity.
369
+ *
370
+ * DNT (Do Not Track) browser header is checked once at init and
371
+ * applied as an automatic deny across all dimensions when
372
+ * `respectDnt: true` is set in CrossdeckOptions (default false because
373
+ * the industry has effectively deprecated DNT — but opt-in support
374
+ * is the polite default for privacy-first apps).
375
+ */
376
+ interface ConsentState {
377
+ analytics: boolean;
378
+ marketing: boolean;
379
+ errors: boolean;
380
+ }
381
+
382
+ /**
383
+ * Breadcrumb ring buffer — context attached to every error report.
384
+ *
385
+ * Sentry / Datadog / Bugsnag all ship the same idea: keep a rolling
386
+ * record of the last N "things the user did" (page views, clicks,
387
+ * custom events, network calls, console logs). When an error fires,
388
+ * attach the buffer so the engineer reading the error can see exactly
389
+ * how the user got into the broken state. The single most powerful
390
+ * debugging signal in error monitoring — without breadcrumbs, errors
391
+ * are stack traces with no story.
392
+ *
393
+ * Implementation: a circular buffer with a fixed cap. Old entries are
394
+ * evicted as new ones arrive. The default cap (50) is enough to cover
395
+ * ~5 minutes of typical user activity without ballooning the error
396
+ * payload — Sentry uses 100 by default but the SDK is more aggressive
397
+ * about size since we ship breadcrumbs over the wire with every error,
398
+ * not as a separate batch.
399
+ *
400
+ * Privacy: breadcrumbs auto-emit from the same auto-tracking sources
401
+ * as analytics events (page.viewed, element.clicked). Those already
402
+ * skip password fields, form inputs, and cd-noTrack subtrees. Custom
403
+ * crumbs added via Crossdeck.addBreadcrumb() pass through the same
404
+ * property sanitiser as track() events.
405
+ */
406
+ type BreadcrumbCategory = "navigation" | "ui.click" | "ui.input" | "http" | "console" | "custom" | "info";
407
+ type BreadcrumbLevel = "debug" | "info" | "warning" | "error";
408
+ interface Breadcrumb {
409
+ /** epoch ms */
410
+ timestamp: number;
411
+ category: BreadcrumbCategory;
412
+ level?: BreadcrumbLevel;
413
+ /** Short human-readable description. */
414
+ message?: string;
415
+ /** Arbitrary key/value context for the crumb. */
416
+ data?: Record<string, unknown>;
417
+ }
418
+
419
+ /**
420
+ * Stack-trace parser — normalises Chrome / Firefox / Safari / Edge
421
+ * stack strings into a common frame shape.
422
+ *
423
+ * Why hand-rolled, not stack-trace-js or error-stack-parser libraries:
424
+ * those weigh 5–15 KB after minification and we'd be pulling in their
425
+ * full feature matrix just for the parser. The patterns below cover
426
+ * the four shapes any modern browser emits, totalling ~80 lines.
427
+ *
428
+ * The output frame shape mirrors what Sentry's `mechanism: { type:
429
+ * 'generic' }` events ship, so future source-map symbolication on the
430
+ * Crossdeck backend has a stable input to work against.
431
+ *
432
+ * Defensive: never throws. An unparseable line becomes a `raw` frame
433
+ * with just the literal text. Engineers reading errors still get the
434
+ * raw stack as fallback.
435
+ */
436
+ interface StackFrame {
437
+ /** Function name, or "?" if anonymous / unparseable. */
438
+ function: string;
439
+ /** Source file URL the frame ran in. Empty when unknown. */
440
+ filename: string;
441
+ /** 1-indexed line number, or 0 when unknown. */
442
+ lineno: number;
443
+ /** 1-indexed column number, or 0 when unknown. */
444
+ colno: number;
445
+ /**
446
+ * True when the frame is in the app's own code (best-effort:
447
+ * detected by URL not starting with chrome-extension://, etc.).
448
+ * Helps the dashboard's "your code vs library code" view.
449
+ */
450
+ in_app: boolean;
451
+ /** Raw line from the stack string for debugging when parse fails. */
452
+ raw: string;
453
+ }
454
+
455
+ /**
456
+ * Error capture — the third Crossdeck USP.
457
+ *
458
+ * Catches every error source the browser can hand us and ships them as
459
+ * Crossdeck events. The pipeline reuses the analytics queue:
460
+ * - Same durable persistence (errors survive crashes / hard closes)
461
+ * - Same exponential backoff (a flapping server doesn't flood
462
+ * errors past the rate limit)
463
+ * - Same Idempotency-Key (duplicate batches dedup server-side)
464
+ * - Same consent gate (consent.errors)
465
+ * - Same PII scrub on properties before they leave
466
+ *
467
+ * Error sources captured (each toggleable):
468
+ * 1. window.onerror — uncaught synchronous errors
469
+ * 2. window.onunhandledrejection — unhandled promise rejections
470
+ * 3. fetch() wrap — HTTP errors the app code didn't catch
471
+ * 4. XMLHttpRequest wrap — same, for legacy XHR consumers
472
+ * 5. Crossdeck.captureError(err) — manual API for try/catch blocks
473
+ * 6. Crossdeck.captureMessage(msg) — non-error events you want to
474
+ * surface as issues (e.g. "we hit the soft-deprecated path")
475
+ *
476
+ * Defensive design rules:
477
+ * - The error handler must NEVER throw — if our own code crashes
478
+ * while reporting an error, we'd take down the host app's error
479
+ * handler too. Every callback is wrapped in try/swallow.
480
+ * - Recursion guard: a `_reporting` flag prevents the SDK from
481
+ * reporting its own errors recursively forever.
482
+ * - Rate limited per-fingerprint: max N reports per second to defend
483
+ * against runaway loops (e.g. an error in setInterval).
484
+ * - Browser-extension noise is filtered by default — those errors
485
+ * aren't the developer's fault and would otherwise drown the
486
+ * signal.
487
+ */
488
+
489
+ type ErrorLevel = "error" | "warning" | "info";
490
+ interface CapturedError {
491
+ /** When the error fired (epoch ms). */
492
+ timestamp: number;
493
+ /** error.unhandled, error.unhandledrejection, error.handled, error.message, error.http */
494
+ kind: "error.unhandled" | "error.unhandledrejection" | "error.handled" | "error.message" | "error.http";
495
+ level: ErrorLevel;
496
+ message: string;
497
+ /** The error class name when we have it (TypeError, ReferenceError, etc.) */
498
+ errorType: string | null;
499
+ /** Parsed stack frames, empty when unavailable. */
500
+ frames: StackFrame[];
501
+ /** Raw stack string for fallback display. */
502
+ rawStack: string | null;
503
+ /** Origin URL when available (window.onerror's `source` arg). */
504
+ filename: string | null;
505
+ lineno: number | null;
506
+ colno: number | null;
507
+ /** djb2 hash of message + top frames — group identical errors. */
508
+ fingerprint: string;
509
+ /** Snapshot of the breadcrumb buffer at the moment the error fired. */
510
+ breadcrumbs: Breadcrumb[];
511
+ /** Free-form context attached via Crossdeck.setContext(). */
512
+ context: Record<string, unknown>;
513
+ /** Free-form tags attached via Crossdeck.setTag(). */
514
+ tags: Record<string, string>;
515
+ /** "TypeError: x is not a function" → "TypeError" + "x is not a function". */
516
+ /** Whether the error happened during a fetch / XHR. */
517
+ http?: {
518
+ url: string;
519
+ method: string;
520
+ status: number;
521
+ statusText?: string;
522
+ };
523
+ }
524
+
242
525
  /**
243
526
  * Public API surface for @cross-deck/web.
244
527
  *
@@ -294,8 +577,145 @@ declare class CrossdeckClient {
294
577
  /**
295
578
  * Link the anonymous device to a developer-supplied user ID. Cache
296
579
  * the resolved Crossdeck customer for follow-up calls.
580
+ *
581
+ * v0.9.0+ accepts an optional `traits` bag — profile data (name,
582
+ * plan, signupDate, role) persisted on the Crossdeck customer record
583
+ * and queryable from dashboards. Traits are sanitised through the
584
+ * same validator that gates `track()` properties, so a `{ avatar:
585
+ * <File>, onSave: () => {} }` payload can't corrupt the alias call.
586
+ *
587
+ * Crossdeck.identify("user_847", {
588
+ * email: "wes@pinet.co.za",
589
+ * traits: { name: "Wes", plan: "pro", signedUpAt: "2026-05-11" },
590
+ * });
591
+ */
592
+ identify(userId: string, options?: IdentifyOptions): Promise<AliasResult>;
593
+ /**
594
+ * Register super-properties — Mixpanel pattern. Once set, every
595
+ * subsequent event of THIS SDK instance carries these keys on its
596
+ * properties bag automatically.
597
+ *
598
+ * Crossdeck.register({ plan: "pro", releaseChannel: "beta" });
599
+ * Crossdeck.track("paywall_shown"); // includes plan + releaseChannel
600
+ *
601
+ * Values that are `null` are deleted (the explicit "stop tracking
602
+ * this key" idiom). Returns the resulting bag.
603
+ *
604
+ * Sanitised through `validateEventProperties` so a `{ avatar: File }`
605
+ * payload can't poison the queue at flush time.
606
+ */
607
+ register(properties: Record<string, unknown>): Record<string, unknown>;
608
+ /** Remove a single super-property key. Idempotent. */
609
+ unregister(key: string): void;
610
+ /** Snapshot of the current super-property bag. */
611
+ getSuperProperties(): Record<string, unknown>;
612
+ /**
613
+ * Associate the current user with a group (org, team, account, etc.).
614
+ * Mixpanel / Segment "Group Analytics" pattern.
615
+ *
616
+ * Crossdeck.group("org", "acme_inc");
617
+ * Crossdeck.group("team", "design", { headcount: 12 });
618
+ *
619
+ * Once set, every subsequent event carries `$groups.<type>: id` on
620
+ * its properties bag, enabling B2B dashboards ("how is Acme using
621
+ * the product"). Pass `id: null` to clear a group membership.
622
+ */
623
+ group(type: string, id: string | null, traits?: GroupTraits): void;
624
+ /** Snapshot of the current groups map keyed by type. */
625
+ getGroups(): Record<string, {
626
+ id: string;
627
+ traits?: Record<string, unknown>;
628
+ }>;
629
+ /**
630
+ * Update consent state. Three independent dimensions:
631
+ *
632
+ * analytics — track() + identify() + auto-emissions
633
+ * marketing — paid-traffic click IDs + referrer URL on events
634
+ * errors — Web Vitals + (future) error reporting
635
+ *
636
+ * Each defaults to `true` (granted). Pass partial state — only the
637
+ * keys you provide are changed.
638
+ *
639
+ * Crossdeck.consent({ analytics: false });
640
+ * Crossdeck.consent({ marketing: true, errors: true });
641
+ *
642
+ * DNT-derived denies cannot be flipped back on; if the browser said
643
+ * "don't track" we don't track even if the developer code disagrees.
644
+ */
645
+ consent(state: Partial<ConsentState>): ConsentState;
646
+ /** Snapshot of the current consent state. */
647
+ consentStatus(): ConsentState;
648
+ /**
649
+ * Manually capture an error from a try/catch block.
650
+ *
651
+ * try { …risky… } catch (err) {
652
+ * Crossdeck.captureError(err, { context: { plan: "pro" } });
653
+ * }
654
+ *
655
+ * The error is shipped through the same event queue as analytics
656
+ * (durable, retried, rate-limited per fingerprint). Sends are gated
657
+ * by `consent.errors`. Returns silently — never throws, even if the
658
+ * SDK isn't initialised yet.
297
659
  */
298
- identify(userId: string, _options?: IdentifyOptions): Promise<AliasResult>;
660
+ captureError(error: unknown, options?: {
661
+ context?: Record<string, unknown>;
662
+ tags?: Record<string, string>;
663
+ level?: ErrorLevel;
664
+ }): void;
665
+ /**
666
+ * Capture a non-error event you want to surface as an issue
667
+ * ("deprecated path hit", "we entered the slow code path"). Sentry
668
+ * captureMessage pattern. Returns silently if not initialised.
669
+ */
670
+ captureMessage(message: string, level?: ErrorLevel): void;
671
+ /**
672
+ * Attach a tag to every subsequent error report. Tags are key/value
673
+ * strings (Sentry pattern): `setTag("flow", "checkout")` → every
674
+ * error from this point on carries `tags.flow === "checkout"`.
675
+ */
676
+ setTag(key: string, value: string): void;
677
+ /** Bulk-set tags. Merges with existing tags. */
678
+ setTags(tags: Record<string, string>): void;
679
+ /**
680
+ * Attach a structured context blob to every subsequent error report.
681
+ * Unlike tags (flat key/value), context is a named bag of arbitrary
682
+ * data: `setContext("cart", { items: 3, total: 42.99 })`.
683
+ */
684
+ setContext(name: string, data: Record<string, unknown>): void;
685
+ /**
686
+ * Add a custom breadcrumb to the rolling buffer. Useful for marking
687
+ * domain-meaningful moments ("user opened paywall") that aren't
688
+ * already auto-captured. The buffer caps at 50 entries; old ones
689
+ * evict.
690
+ */
691
+ addBreadcrumb(crumb: Breadcrumb): void;
692
+ /**
693
+ * Install a pre-send hook for errors. Return null to drop, or a
694
+ * modified CapturedError to scrub / rewrite. Sentry's beforeSend
695
+ * pattern — the only way to redact app-specific PII (auth tokens
696
+ * in URLs, etc.) before the report leaves the browser.
697
+ */
698
+ setErrorBeforeSend(hook: ((err: CapturedError) => CapturedError | null) | null): void;
699
+ /**
700
+ * Internal: turn a CapturedError into a Crossdeck event and enqueue
701
+ * it. Goes through the same queue / persistence / consent / scrub
702
+ * pipeline as analytics events.
703
+ */
704
+ private reportError;
705
+ /**
706
+ * GDPR/CCPA "right to be forgotten" — calls the backend's
707
+ * /v1/identity/forget endpoint to schedule a server-side deletion of
708
+ * the customer's events and profile, then wipes all local state
709
+ * (identity, entitlements, queue, super-props, persistent stores).
710
+ *
711
+ * Idempotent. Safe to call when no identity has been established
712
+ * (it just wipes the empty local state).
713
+ *
714
+ * After forget() resolves, the SDK is in the same shape as if the
715
+ * developer had called `Crossdeck.reset()` — a fresh anonymousId is
716
+ * minted and the next session is a brand new identity-graph entry.
717
+ */
718
+ forget(): Promise<void>;
299
719
  /**
300
720
  * Read the current customer's active entitlements from the server.
301
721
  * Updates the local cache so subsequent isEntitled() calls answer
@@ -458,12 +878,21 @@ interface CrossdeckErrorPayload {
458
878
  requestId?: string;
459
879
  /** HTTP status code if the error came from an API response. */
460
880
  status?: number;
881
+ /**
882
+ * Server-suggested wait (in milliseconds) before retrying. Populated
883
+ * from the `Retry-After` response header on 429 / 503. The header
884
+ * spec allows either delta-seconds or an HTTP-date; the parser below
885
+ * normalises both to milliseconds. Consumers MUST honour this — the
886
+ * server is telling you the safe rate.
887
+ */
888
+ retryAfterMs?: number;
461
889
  }
462
890
  declare class CrossdeckError extends Error {
463
891
  readonly type: CrossdeckErrorType;
464
892
  readonly code: string;
465
893
  readonly requestId?: string;
466
894
  readonly status?: number;
895
+ readonly retryAfterMs?: number;
467
896
  constructor(payload: CrossdeckErrorPayload);
468
897
  }
469
898
 
@@ -526,9 +955,45 @@ declare class MemoryStorage implements KeyValueStorage {
526
955
  * fetch shim, no transitive deps.
527
956
  */
528
957
  declare const SDK_NAME = "@cross-deck/web";
529
- declare const SDK_VERSION = "0.6.0";
958
+ declare const SDK_VERSION = "1.0.0";
530
959
  declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
531
960
 
961
+ /**
962
+ * Machine-readable index of every error code the SDK can throw, with
963
+ * a short description and a hint on what action to take. Published
964
+ * verbatim as `crossdeck-error-codes.json` in the npm tarball so AI
965
+ * integration assistants, error-aggregator dashboards (Sentry,
966
+ * DataDog), and the Crossdeck dashboard can render human-friendly
967
+ * messages without parsing freeform `message` strings.
968
+ *
969
+ * Stripe publishes the same surface at stripe.com/docs/error-codes;
970
+ * developers love it because every code has a canonical "what does
971
+ * this mean / what should I do" answer.
972
+ *
973
+ * Adding a new error code:
974
+ * 1. Add the code string to the union in `errors.ts` (where used).
975
+ * 2. Add an entry here.
976
+ * 3. The next `npm run build` regenerates the JSON sidecar.
977
+ *
978
+ * Keep entries terse — the consumer surfaces this in tooltips and
979
+ * automated tickets, not in long-form docs.
980
+ */
981
+ interface ErrorCodeEntry {
982
+ /** The string thrown as CrossdeckError.code. */
983
+ code: string;
984
+ /** CrossdeckError.type — broad category. */
985
+ type: "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "internal_error" | "network_error" | "configuration_error";
986
+ /** One-sentence description. Surfaced verbatim in dashboards. */
987
+ description: string;
988
+ /** What the developer should do. Imperative phrasing. */
989
+ resolution: string;
990
+ /** True for codes the SDK can auto-recover from (no developer action). */
991
+ retryable: boolean;
992
+ }
993
+ declare const CROSSDECK_ERROR_CODES: readonly ErrorCodeEntry[];
994
+ /** Lookup helper — returns the entry matching a CrossdeckError.code, or undefined. */
995
+ declare function getErrorCode(code: string): ErrorCodeEntry | undefined;
996
+
532
997
  /**
533
998
  * Device + environment enrichment.
534
999
  *
@@ -563,4 +1028,4 @@ interface DeviceInfo {
563
1028
  appVersion?: string;
564
1029
  }
565
1030
 
566
- 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 };
1031
+ export { type AliasResult, type AuditRail, type AutoTrackOptions, type Breadcrumb, type BreadcrumbCategory, type BreadcrumbLevel, CROSSDECK_ERROR_CODES, type CapturedError, 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 ErrorLevel, type EventProperties, type GroupTraits, type HeartbeatResponse, type IdentifyOptions, type KeyValueStorage, MemoryStorage, type Platform, type PublicEntitlement, type PurchaseResult, SDK_NAME, SDK_VERSION, type StackFrame, getErrorCode };