@cross-deck/web 1.5.0 → 1.5.1
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/CHANGELOG.md +74 -0
- package/README.md +94 -0
- package/dist/contracts 2.json +378 -0
- package/dist/contracts.json +1 -1
- package/dist/crossdeck.umd.min 2.js +3 -0
- package/dist/crossdeck.umd.min.js 2.map +1 -0
- package/dist/error-codes 2.json +196 -0
- package/dist/error-codes.json +1 -1
- package/dist/index 2.cjs +4778 -0
- package/dist/index 2.mjs +4742 -0
- package/dist/index.cjs 2.map +1 -0
- package/dist/index.d 2.mts +938 -0
- package/dist/index.d 2.ts +938 -0
- package/dist/index.mjs 2.map +1 -0
- package/dist/react 2.cjs +4220 -0
- package/dist/react 2.mjs +4193 -0
- package/dist/react.cjs 2.map +1 -0
- package/dist/react.d 2.mts +91 -0
- package/dist/react.d 2.ts +91 -0
- package/dist/react.mjs 2.map +1 -0
- package/dist/types-Bu3jbmdq.d 2.mts +314 -0
- package/dist/types-Bu3jbmdq.d 2.ts +314 -0
- package/dist/vue 2.cjs +4185 -0
- package/dist/vue 2.mjs +4159 -0
- package/dist/vue.cjs 2.map +1 -0
- package/dist/vue.d 2.mts +37 -0
- package/dist/vue.d 2.ts +37 -0
- package/dist/vue.mjs 2.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
import { P as PublicEntitlement, C as CrossdeckOptions, I as IdentifyOptions, A as AliasResult, G as GroupTraits, E as EventProperties, a as PurchaseResult, H as HeartbeatResponse, D as Diagnostics, K as KeyValueStorage } from './types-Bu3jbmdq.mjs';
|
|
2
|
+
export { b as AuditRail, c as AutoTrackOptions, d as EntitlementsListResponse, e as Environment, f as Platform } from './types-Bu3jbmdq.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Durable last-known-good cache of the customer's entitlements.
|
|
6
|
+
*
|
|
7
|
+
* This cache is NOT a second source of truth. Crossdeck remains the
|
|
8
|
+
* only source; this is the SDK's local copy of what the server last
|
|
9
|
+
* told us — a cache that doesn't forget during a network partition.
|
|
10
|
+
*
|
|
11
|
+
* Durability contract (the RevenueCat model):
|
|
12
|
+
* - Every successful server read is persisted to device storage
|
|
13
|
+
* (localStorage, via the SDK's storage adapter).
|
|
14
|
+
* - On SDK boot the cache hydrates from storage synchronously, so
|
|
15
|
+
* isEntitled() answers correctly from the very first call — there
|
|
16
|
+
* is no cold-start window where a returning Pro customer reads as
|
|
17
|
+
* free.
|
|
18
|
+
* - When the server is unreachable, the SDK keeps serving the last
|
|
19
|
+
* entitlements it successfully fetched. A failed refresh never
|
|
20
|
+
* reaches setFromList(), so it cannot clear the cache; only a
|
|
21
|
+
* SUCCESSFUL fetch replaces it. An outage can never fail a paying
|
|
22
|
+
* customer down to free.
|
|
23
|
+
* - Staleness alone never returns false. Each entitlement is honoured
|
|
24
|
+
* against its OWN validUntil instead — a time-based trial expiry
|
|
25
|
+
* still applies even mid-partition, a still-valid Pro entitlement
|
|
26
|
+
* rides the outage out.
|
|
27
|
+
* - Staleness is VISIBLE, not silent. validUntil covers time-based
|
|
28
|
+
* expiry; it does NOT cover an event-based revoke (chargeback,
|
|
29
|
+
* refund, fraud) — that has no validUntil, so the cache would keep
|
|
30
|
+
* serving a revoked customer through an outage. Serving them is the
|
|
31
|
+
* right trade (don't lock real payers out), but unbounded-and-
|
|
32
|
+
* invisible is the bug. So: once a refresh ATTEMPT fails (or the
|
|
33
|
+
* data ages past staleAfterMs) the cache is marked stale —
|
|
34
|
+
* isStale / freshness are surfaced in diagnostics(). It keeps
|
|
35
|
+
* serving last-known-good; the staleness is just no longer hidden.
|
|
36
|
+
*
|
|
37
|
+
* The cache is wiped only on reset() (logout) and on an identity switch
|
|
38
|
+
* — never by a TTL.
|
|
39
|
+
*
|
|
40
|
+
* Reactive listener API
|
|
41
|
+
* ---------------------
|
|
42
|
+
* `subscribe(listener)` registers a callback fired every time the cache
|
|
43
|
+
* mutates (setFromList or clear) — the foundation for the
|
|
44
|
+
* `useEntitlement` React hook and other framework bindings. Semantics:
|
|
45
|
+
* - Fired AFTER the mutation, so the listener sees fresh state.
|
|
46
|
+
* - Fire-and-forget: a throwing listener is swallowed (and counted)
|
|
47
|
+
* so a buggy consumer can't crash the SDK or other listeners.
|
|
48
|
+
* - Unsubscribe is idempotent.
|
|
49
|
+
* - Listeners are NOT fired on subscribe — a caller that wants the
|
|
50
|
+
* initial state reads isEntitled() / list() synchronously, which
|
|
51
|
+
* work from boot thanks to hydration above.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
type EntitlementsListener = (entitlements: PublicEntitlement[]) => void;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Consent gating — GDPR / CCPA-grade kill switches.
|
|
58
|
+
*
|
|
59
|
+
* Three independent dimensions, each defaulting to "granted" but
|
|
60
|
+
* runtime-overridable:
|
|
61
|
+
*
|
|
62
|
+
* analytics — track(), identify(), heartbeat(), session/page auto-
|
|
63
|
+
* emissions. Off → events drop silently, no network
|
|
64
|
+
* calls fire.
|
|
65
|
+
* marketing — paid-traffic click IDs (gclid/fbclid/etc) and
|
|
66
|
+
* acquisition referrer URL. Off → these get scrubbed
|
|
67
|
+
* before they ever land in the event bag.
|
|
68
|
+
* errors — error / breadcrumb / Web Vitals capture. Off → no
|
|
69
|
+
* webvitals.* events emitted, no error reporting (when
|
|
70
|
+
* Phase 3 errors land).
|
|
71
|
+
*
|
|
72
|
+
* Why this granularity: real consent banners offer "Analytics",
|
|
73
|
+
* "Marketing", "Functional" as separate boxes. The SDK has to match.
|
|
74
|
+
*
|
|
75
|
+
* Default state: every dimension is granted. The developer must
|
|
76
|
+
* explicitly call `Crossdeck.consent({ analytics: false })` before
|
|
77
|
+
* the first event to opt OUT — same convention as Google Tag Manager
|
|
78
|
+
* Consent Mode. To start in deny mode, call `init(...)` then
|
|
79
|
+
* immediately `consent({ analytics: false, marketing: false, errors:
|
|
80
|
+
* false })` before any user activity.
|
|
81
|
+
*
|
|
82
|
+
* DNT (Do Not Track) browser header is checked once at init and
|
|
83
|
+
* applied as an automatic deny across all dimensions when
|
|
84
|
+
* `respectDnt: true` is set in CrossdeckOptions (default false because
|
|
85
|
+
* the industry has effectively deprecated DNT — but opt-in support
|
|
86
|
+
* is the polite default for privacy-first apps).
|
|
87
|
+
*/
|
|
88
|
+
interface ConsentState {
|
|
89
|
+
analytics: boolean;
|
|
90
|
+
marketing: boolean;
|
|
91
|
+
errors: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Breadcrumb ring buffer — context attached to every error report.
|
|
96
|
+
*
|
|
97
|
+
* Sentry / Datadog / Bugsnag all ship the same idea: keep a rolling
|
|
98
|
+
* record of the last N "things the user did" (page views, clicks,
|
|
99
|
+
* custom events, network calls, console logs). When an error fires,
|
|
100
|
+
* attach the buffer so the engineer reading the error can see exactly
|
|
101
|
+
* how the user got into the broken state. The single most powerful
|
|
102
|
+
* debugging signal in error monitoring — without breadcrumbs, errors
|
|
103
|
+
* are stack traces with no story.
|
|
104
|
+
*
|
|
105
|
+
* Implementation: a circular buffer with a fixed cap. Old entries are
|
|
106
|
+
* evicted as new ones arrive. The default cap (50) is enough to cover
|
|
107
|
+
* ~5 minutes of typical user activity without ballooning the error
|
|
108
|
+
* payload — Sentry uses 100 by default but the SDK is more aggressive
|
|
109
|
+
* about size since we ship breadcrumbs over the wire with every error,
|
|
110
|
+
* not as a separate batch.
|
|
111
|
+
*
|
|
112
|
+
* Privacy: breadcrumbs auto-emit from the same auto-tracking sources
|
|
113
|
+
* as analytics events (page.viewed, element.clicked). Those already
|
|
114
|
+
* skip password fields, form inputs, and cd-noTrack subtrees. Custom
|
|
115
|
+
* crumbs added via Crossdeck.addBreadcrumb() pass through the same
|
|
116
|
+
* property sanitiser as track() events.
|
|
117
|
+
*/
|
|
118
|
+
type BreadcrumbCategory = "navigation" | "ui.click" | "ui.input" | "http" | "console" | "custom" | "info";
|
|
119
|
+
type BreadcrumbLevel = "debug" | "info" | "warning" | "error";
|
|
120
|
+
interface Breadcrumb {
|
|
121
|
+
/** epoch ms */
|
|
122
|
+
timestamp: number;
|
|
123
|
+
category: BreadcrumbCategory;
|
|
124
|
+
level?: BreadcrumbLevel;
|
|
125
|
+
/** Short human-readable description. */
|
|
126
|
+
message?: string;
|
|
127
|
+
/** Arbitrary key/value context for the crumb. */
|
|
128
|
+
data?: Record<string, unknown>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Public, typed accessor for the bank-grade behavioural contracts
|
|
133
|
+
* this SDK ships. The full architecture — schema, distribution,
|
|
134
|
+
* audit loop, pillar taxonomy — lives in `contracts/README.md`
|
|
135
|
+
* at the monorepo root.
|
|
136
|
+
*
|
|
137
|
+
* Why a typed surface (vs. plain JSON access): contract IDs and
|
|
138
|
+
* pillar names are part of Crossdeck's public commitment to
|
|
139
|
+
* customers. Reading them through `CrossdeckContracts` means the
|
|
140
|
+
* compiler catches drift the moment a contract is renamed or
|
|
141
|
+
* retired. Tools that consume contracts at runtime (dashboards,
|
|
142
|
+
* AI assistants, customer integration tests) get the exact same
|
|
143
|
+
* shape every SDK ships, with no parsing layer to drift.
|
|
144
|
+
*
|
|
145
|
+
* --- BINARY STABILITY ---
|
|
146
|
+
* `Contract` is treated as an evolving — but back-compat — wire
|
|
147
|
+
* shape. Fields may be added in any minor release. Existing
|
|
148
|
+
* fields will not be removed or repurposed except in a major
|
|
149
|
+
* version bump, even if all known contracts stop using them.
|
|
150
|
+
* Customers can rely on `id`, `pillar`, `status`, `appliesTo`,
|
|
151
|
+
* `codeRef`, `testRef`, `registeredAt`, `firstRegisteredIn`,
|
|
152
|
+
* and `bundledIn` being present on every contract in every
|
|
153
|
+
* future minor/patch release of this SDK.
|
|
154
|
+
*/
|
|
155
|
+
/**
|
|
156
|
+
* Which bank-grade pillar a contract belongs to. The taxonomy is
|
|
157
|
+
* deliberately small — every contract maps to exactly one. New
|
|
158
|
+
* pillars require a Crossdeck major-version bump.
|
|
159
|
+
*/
|
|
160
|
+
type ContractPillar = "revenue" | "entitlements" | "analytics" | "webhooks" | "errors" | "lifecycle" | "identity";
|
|
161
|
+
/**
|
|
162
|
+
* Lifecycle stage of a contract.
|
|
163
|
+
* - `enforced`: live in this SDK and exercised by `testRef`.
|
|
164
|
+
* - `proposed`: registered for an upcoming release; `testRef`
|
|
165
|
+
* may point to a not-yet-existing file.
|
|
166
|
+
* - `retired`: kept for history only; the behaviour no longer
|
|
167
|
+
* ships. Filtered out of `CrossdeckContracts.all()` by default.
|
|
168
|
+
*/
|
|
169
|
+
type ContractStatus = "enforced" | "proposed" | "retired";
|
|
170
|
+
/** Which SDKs (and/or `backend`) a contract is binding on. */
|
|
171
|
+
type ContractAppliesTo = "web" | "node" | "react-native" | "swift" | "android" | "backend";
|
|
172
|
+
/**
|
|
173
|
+
* Pointer to the test that exercises a contract clause. The
|
|
174
|
+
* `name` is matched verbatim against the file's text by
|
|
175
|
+
* `scripts/contract-audit.mjs`, so a rename without updating
|
|
176
|
+
* the contract aborts CI.
|
|
177
|
+
*/
|
|
178
|
+
interface ContractTestRef {
|
|
179
|
+
readonly file: string;
|
|
180
|
+
readonly name: string;
|
|
181
|
+
}
|
|
182
|
+
/** One bank-grade behavioural guarantee — see `contracts/README.md`. */
|
|
183
|
+
interface Contract {
|
|
184
|
+
readonly id: string;
|
|
185
|
+
readonly pillar: ContractPillar;
|
|
186
|
+
readonly status: ContractStatus;
|
|
187
|
+
readonly claim: string;
|
|
188
|
+
readonly appliesTo: readonly ContractAppliesTo[];
|
|
189
|
+
readonly codeRef: readonly string[];
|
|
190
|
+
readonly testRef: readonly ContractTestRef[];
|
|
191
|
+
/** ISO-8601 date the contract was first registered. */
|
|
192
|
+
readonly registeredAt: string;
|
|
193
|
+
/** The release note / phase the contract first appeared in. Immutable. */
|
|
194
|
+
readonly firstRegisteredIn: string;
|
|
195
|
+
/** The SDK release this snapshot was bundled with, stamped at build time. */
|
|
196
|
+
readonly bundledIn: string;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Typed entry point to the bank-grade contracts bundled with this
|
|
200
|
+
* SDK release. Stable, side-effect-free, tree-shakeable.
|
|
201
|
+
*
|
|
202
|
+
* @example Audit at app boot
|
|
203
|
+
* ```ts
|
|
204
|
+
* import { CrossdeckContracts } from "@cross-deck/web";
|
|
205
|
+
*
|
|
206
|
+
* for (const c of CrossdeckContracts.all()) {
|
|
207
|
+
* console.log(`[crossdeck] ${c.id} (${c.pillar})`);
|
|
208
|
+
* }
|
|
209
|
+
* ```
|
|
210
|
+
*
|
|
211
|
+
* @example Assert a specific clause is in force
|
|
212
|
+
* ```ts
|
|
213
|
+
* const isolation = CrossdeckContracts.byId("per-user-cache-isolation");
|
|
214
|
+
* if (!isolation || isolation.status !== "enforced") {
|
|
215
|
+
* throw new Error("entitlement isolation contract is not enforced — refusing to start");
|
|
216
|
+
* }
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
declare const CrossdeckContracts: {
|
|
220
|
+
/** Every contract that applies to this SDK and is currently enforced. */
|
|
221
|
+
readonly all: () => readonly Contract[];
|
|
222
|
+
/**
|
|
223
|
+
* Every contract bundled with this SDK release, including
|
|
224
|
+
* `proposed` and `retired` entries. Use `all()` for the
|
|
225
|
+
* enforced-only view.
|
|
226
|
+
*/
|
|
227
|
+
readonly allIncludingHistorical: () => readonly Contract[];
|
|
228
|
+
/** Look up a contract by its stable `id`. */
|
|
229
|
+
readonly byId: (id: string) => Contract | undefined;
|
|
230
|
+
/** Every enforced contract within a pillar. */
|
|
231
|
+
readonly byPillar: (pillar: ContractPillar) => readonly Contract[];
|
|
232
|
+
/** Filter by lifecycle status. */
|
|
233
|
+
readonly withStatus: (status: ContractStatus) => readonly Contract[];
|
|
234
|
+
/** Semver of the SDK release these contracts were bundled with. */
|
|
235
|
+
readonly sdkVersion: "1.5.0";
|
|
236
|
+
/** Fully-qualified bundle identifier — e.g. `@cross-deck/web@1.4.2`. */
|
|
237
|
+
readonly bundledIn: "@cross-deck/web@1.5.0";
|
|
238
|
+
/**
|
|
239
|
+
* Resolve a failing test back to the contract it exercises.
|
|
240
|
+
* Used by test-framework hooks (Vitest `afterEach`, XCTest
|
|
241
|
+
* observation, JUnit `TestWatcher`) to find the contract id of
|
|
242
|
+
* a failed contract test so `reportContractFailure(...)` can
|
|
243
|
+
* stamp the right `contract_id` on the emitted event.
|
|
244
|
+
*
|
|
245
|
+
* Match is on `testRef.name` (case-sensitive, exact). Returns
|
|
246
|
+
* the first contract whose `testRef` list contains a matching
|
|
247
|
+
* entry, regardless of pillar or status.
|
|
248
|
+
*/
|
|
249
|
+
readonly findByTestName: (name: string) => Contract | undefined;
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* Input to {@link Crossdeck.reportContractFailure}. Lets a test
|
|
253
|
+
* harness / dogfood app / customer integration report a contract
|
|
254
|
+
* violation back to Crossdeck through the standard `track()` API
|
|
255
|
+
* with a typed property bag — no new ingest endpoint, no bespoke
|
|
256
|
+
* code, just a `crossdeck.contract_failed` custom event.
|
|
257
|
+
*
|
|
258
|
+
* `sdk_version` and `sdk_platform` are auto-stamped by the SDK so
|
|
259
|
+
* every emitted event carries them correctly without the caller
|
|
260
|
+
* needing to read them out of `CrossdeckContracts.sdkVersion`.
|
|
261
|
+
*/
|
|
262
|
+
interface ContractFailureInput {
|
|
263
|
+
/** Stable contract id (`per-user-cache-isolation` etc.). */
|
|
264
|
+
contractId: string;
|
|
265
|
+
/** Human-readable failure reason — the assertion message. */
|
|
266
|
+
failureReason: string;
|
|
267
|
+
/**
|
|
268
|
+
* Where the failure was observed:
|
|
269
|
+
* - `ci` — the SDK's own test suite on CI
|
|
270
|
+
* - `dogfood` — Crossdeck's internal dogfood project
|
|
271
|
+
* - `customer-app` — a customer's app verifying contracts
|
|
272
|
+
*/
|
|
273
|
+
runContext: "ci" | "dogfood" | "customer-app";
|
|
274
|
+
/**
|
|
275
|
+
* Stable identifier for this verification run. CI: `GITHUB_RUN_ID`
|
|
276
|
+
* or equivalent. Dogfood: per-launch UUID. Customer app: any
|
|
277
|
+
* stable handle the customer chooses to group fires by run.
|
|
278
|
+
*/
|
|
279
|
+
runId: string;
|
|
280
|
+
/**
|
|
281
|
+
* Optional pointer back to the failing test, for triage. The SDK
|
|
282
|
+
* sends both `test_file` and `test_name` on the wire when set.
|
|
283
|
+
*/
|
|
284
|
+
testRef?: {
|
|
285
|
+
file: string;
|
|
286
|
+
name: string;
|
|
287
|
+
};
|
|
288
|
+
/** Optional extra properties merged into the event payload. */
|
|
289
|
+
extra?: Record<string, unknown>;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Stack-trace parser — normalises Chrome / Firefox / Safari / Edge
|
|
294
|
+
* stack strings into a common frame shape.
|
|
295
|
+
*
|
|
296
|
+
* Why hand-rolled, not stack-trace-js or error-stack-parser libraries:
|
|
297
|
+
* those weigh 5–15 KB after minification and we'd be pulling in their
|
|
298
|
+
* full feature matrix just for the parser. The patterns below cover
|
|
299
|
+
* the four shapes any modern browser emits, totalling ~80 lines.
|
|
300
|
+
*
|
|
301
|
+
* The output frame shape mirrors what Sentry's `mechanism: { type:
|
|
302
|
+
* 'generic' }` events ship, so future source-map symbolication on the
|
|
303
|
+
* Crossdeck backend has a stable input to work against.
|
|
304
|
+
*
|
|
305
|
+
* Defensive: never throws. An unparseable line becomes a `raw` frame
|
|
306
|
+
* with just the literal text. Engineers reading errors still get the
|
|
307
|
+
* raw stack as fallback.
|
|
308
|
+
*/
|
|
309
|
+
interface StackFrame {
|
|
310
|
+
/** Function name, or "?" if anonymous / unparseable. */
|
|
311
|
+
function: string;
|
|
312
|
+
/** Source file URL the frame ran in. Empty when unknown. */
|
|
313
|
+
filename: string;
|
|
314
|
+
/** 1-indexed line number, or 0 when unknown. */
|
|
315
|
+
lineno: number;
|
|
316
|
+
/** 1-indexed column number, or 0 when unknown. */
|
|
317
|
+
colno: number;
|
|
318
|
+
/**
|
|
319
|
+
* True when the frame is in the app's own code (best-effort:
|
|
320
|
+
* detected by URL not starting with chrome-extension://, etc.).
|
|
321
|
+
* Helps the dashboard's "your code vs library code" view.
|
|
322
|
+
*/
|
|
323
|
+
in_app: boolean;
|
|
324
|
+
/** Raw line from the stack string for debugging when parse fails. */
|
|
325
|
+
raw: string;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Error capture — the third Crossdeck USP.
|
|
330
|
+
*
|
|
331
|
+
* Catches every error source the browser can hand us and ships them as
|
|
332
|
+
* Crossdeck events. The pipeline reuses the analytics queue:
|
|
333
|
+
* - Same durable persistence (errors survive crashes / hard closes)
|
|
334
|
+
* - Same exponential backoff (a flapping server doesn't flood
|
|
335
|
+
* errors past the rate limit)
|
|
336
|
+
* - Same Idempotency-Key (duplicate batches dedup server-side)
|
|
337
|
+
* - Same consent gate (consent.errors)
|
|
338
|
+
* - Same PII scrub on properties before they leave
|
|
339
|
+
*
|
|
340
|
+
* Error sources captured (each toggleable):
|
|
341
|
+
* 1. window.onerror — uncaught synchronous errors
|
|
342
|
+
* 2. window.onunhandledrejection — unhandled promise rejections
|
|
343
|
+
* 3. fetch() wrap — HTTP errors the app code didn't catch
|
|
344
|
+
* 4. XMLHttpRequest wrap — same, for legacy XHR consumers
|
|
345
|
+
* 5. Crossdeck.captureError(err) — manual API for try/catch blocks
|
|
346
|
+
* 6. Crossdeck.captureMessage(msg) — non-error events you want to
|
|
347
|
+
* surface as issues (e.g. "we hit the soft-deprecated path")
|
|
348
|
+
*
|
|
349
|
+
* Defensive design rules:
|
|
350
|
+
* - The error handler must NEVER throw — if our own code crashes
|
|
351
|
+
* while reporting an error, we'd take down the host app's error
|
|
352
|
+
* handler too. Every callback is wrapped in try/swallow.
|
|
353
|
+
* - Recursion guard: a `_reporting` flag prevents the SDK from
|
|
354
|
+
* reporting its own errors recursively forever.
|
|
355
|
+
* - Rate limited per-fingerprint: max N reports per second to defend
|
|
356
|
+
* against runaway loops (e.g. an error in setInterval).
|
|
357
|
+
* - Browser-extension noise is filtered by default — those errors
|
|
358
|
+
* aren't the developer's fault and would otherwise drown the
|
|
359
|
+
* signal.
|
|
360
|
+
*/
|
|
361
|
+
|
|
362
|
+
type ErrorLevel = "error" | "warning" | "info";
|
|
363
|
+
interface CapturedError {
|
|
364
|
+
/** When the error fired (epoch ms). */
|
|
365
|
+
timestamp: number;
|
|
366
|
+
/** error.unhandled, error.unhandledrejection, error.handled, error.message, error.http */
|
|
367
|
+
kind: "error.unhandled" | "error.unhandledrejection" | "error.handled" | "error.message" | "error.http";
|
|
368
|
+
level: ErrorLevel;
|
|
369
|
+
message: string;
|
|
370
|
+
/** The error class name when we have it (TypeError, ReferenceError, etc.) */
|
|
371
|
+
errorType: string | null;
|
|
372
|
+
/** Parsed stack frames, empty when unavailable. */
|
|
373
|
+
frames: StackFrame[];
|
|
374
|
+
/** Raw stack string for fallback display. */
|
|
375
|
+
rawStack: string | null;
|
|
376
|
+
/** Origin URL when available (window.onerror's `source` arg). */
|
|
377
|
+
filename: string | null;
|
|
378
|
+
lineno: number | null;
|
|
379
|
+
colno: number | null;
|
|
380
|
+
/** djb2 hash of message + top frames — group identical errors. */
|
|
381
|
+
fingerprint: string;
|
|
382
|
+
/** Snapshot of the breadcrumb buffer at the moment the error fired. */
|
|
383
|
+
breadcrumbs: Breadcrumb[];
|
|
384
|
+
/** Free-form context attached via Crossdeck.setContext(). */
|
|
385
|
+
context: Record<string, unknown>;
|
|
386
|
+
/** Free-form tags attached via Crossdeck.setTag(). */
|
|
387
|
+
tags: Record<string, string>;
|
|
388
|
+
/** "TypeError: x is not a function" → "TypeError" + "x is not a function". */
|
|
389
|
+
/** Whether the error happened during a fetch / XHR. */
|
|
390
|
+
http?: {
|
|
391
|
+
url: string;
|
|
392
|
+
method: string;
|
|
393
|
+
status: number;
|
|
394
|
+
statusText?: string;
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Public API surface for @cross-deck/web.
|
|
400
|
+
*
|
|
401
|
+
* Usage (browser):
|
|
402
|
+
*
|
|
403
|
+
* import { Crossdeck } from "@cross-deck/web";
|
|
404
|
+
*
|
|
405
|
+
* Crossdeck.init({
|
|
406
|
+
* appId: "app_web_xxx",
|
|
407
|
+
* publicKey: "cd_pub_live_…",
|
|
408
|
+
* environment: "production",
|
|
409
|
+
* });
|
|
410
|
+
*
|
|
411
|
+
* await Crossdeck.identify("user_847");
|
|
412
|
+
* const ents = await Crossdeck.getEntitlements();
|
|
413
|
+
* if (Crossdeck.isEntitled("pro")) {
|
|
414
|
+
* showPro();
|
|
415
|
+
* }
|
|
416
|
+
* Crossdeck.track("paywall_shown", { variant: "v3" });
|
|
417
|
+
*
|
|
418
|
+
*
|
|
419
|
+
* Usage (Node):
|
|
420
|
+
*
|
|
421
|
+
* import { Crossdeck, MemoryStorage } from "@cross-deck/web";
|
|
422
|
+
*
|
|
423
|
+
* Crossdeck.init({
|
|
424
|
+
* appId: "app_node_xxx",
|
|
425
|
+
* publicKey: "cd_pub_test_…",
|
|
426
|
+
* environment: "sandbox",
|
|
427
|
+
* storage: new MemoryStorage(), // session-only persistence
|
|
428
|
+
* autoHeartbeat: false, // skip the boot ping in scripts
|
|
429
|
+
* });
|
|
430
|
+
*/
|
|
431
|
+
|
|
432
|
+
declare class CrossdeckClient {
|
|
433
|
+
private state;
|
|
434
|
+
/**
|
|
435
|
+
* Boot the SDK. Idempotent — calling init twice with the same options
|
|
436
|
+
* is a no-op; calling with different options replaces the previous
|
|
437
|
+
* configuration.
|
|
438
|
+
*
|
|
439
|
+
* NorthStar §11.1: signature is `Crossdeck.init({ appId, publicKey,
|
|
440
|
+
* environment })`. The trio is validated up-front so a typo'd key or a
|
|
441
|
+
* mismatched env fails fast at boot rather than at first event-flush.
|
|
442
|
+
*/
|
|
443
|
+
init(options: CrossdeckOptions): void;
|
|
444
|
+
/**
|
|
445
|
+
* @deprecated Use `init()` instead. NorthStar §4 standardised the
|
|
446
|
+
* lifecycle method name across SDKs as `init` (formerly `start` /
|
|
447
|
+
* `configure`). `start` will be removed in a future major version.
|
|
448
|
+
*/
|
|
449
|
+
start(options: CrossdeckOptions): void;
|
|
450
|
+
/**
|
|
451
|
+
* Link the anonymous device to a developer-supplied user ID. Cache
|
|
452
|
+
* the resolved Crossdeck customer for follow-up calls.
|
|
453
|
+
*
|
|
454
|
+
* v0.9.0+ accepts an optional `traits` bag — profile data (name,
|
|
455
|
+
* plan, signupDate, role) persisted on the Crossdeck customer record
|
|
456
|
+
* and queryable from dashboards. Traits are sanitised through the
|
|
457
|
+
* same validator that gates `track()` properties, so a `{ avatar:
|
|
458
|
+
* <File>, onSave: () => {} }` payload can't corrupt the alias call.
|
|
459
|
+
*
|
|
460
|
+
* Crossdeck.identify("user_847", {
|
|
461
|
+
* email: "wes@pinet.co.za",
|
|
462
|
+
* traits: { name: "Wes", plan: "pro", signedUpAt: "2026-05-11" },
|
|
463
|
+
* });
|
|
464
|
+
*/
|
|
465
|
+
identify(userId: string, options?: IdentifyOptions): Promise<AliasResult>;
|
|
466
|
+
/**
|
|
467
|
+
* Register super-properties — Mixpanel pattern. Once set, every
|
|
468
|
+
* subsequent event of THIS SDK instance carries these keys on its
|
|
469
|
+
* properties bag automatically.
|
|
470
|
+
*
|
|
471
|
+
* Crossdeck.register({ plan: "pro", releaseChannel: "beta" });
|
|
472
|
+
* Crossdeck.track("paywall_shown"); // includes plan + releaseChannel
|
|
473
|
+
*
|
|
474
|
+
* Values that are `null` are deleted (the explicit "stop tracking
|
|
475
|
+
* this key" idiom). Returns the resulting bag.
|
|
476
|
+
*
|
|
477
|
+
* Sanitised through `validateEventProperties` so a `{ avatar: File }`
|
|
478
|
+
* payload can't poison the queue at flush time.
|
|
479
|
+
*/
|
|
480
|
+
register(properties: Record<string, unknown>): Record<string, unknown>;
|
|
481
|
+
/** Remove a single super-property key. Idempotent. */
|
|
482
|
+
unregister(key: string): void;
|
|
483
|
+
/** Snapshot of the current super-property bag. */
|
|
484
|
+
getSuperProperties(): Record<string, unknown>;
|
|
485
|
+
/**
|
|
486
|
+
* Associate the current user with a group (org, team, account, etc.).
|
|
487
|
+
* Mixpanel / Segment "Group Analytics" pattern.
|
|
488
|
+
*
|
|
489
|
+
* Crossdeck.group("org", "acme_inc");
|
|
490
|
+
* Crossdeck.group("team", "design", { headcount: 12 });
|
|
491
|
+
*
|
|
492
|
+
* Once set, every subsequent event carries `$groups.<type>: id` on
|
|
493
|
+
* its properties bag, enabling B2B dashboards ("how is Acme using
|
|
494
|
+
* the product"). Pass `id: null` to clear a group membership.
|
|
495
|
+
*/
|
|
496
|
+
group(type: string, id: string | null, traits?: GroupTraits): void;
|
|
497
|
+
/** Snapshot of the current groups map keyed by type. */
|
|
498
|
+
getGroups(): Record<string, {
|
|
499
|
+
id: string;
|
|
500
|
+
traits?: Record<string, unknown>;
|
|
501
|
+
}>;
|
|
502
|
+
/**
|
|
503
|
+
* Update consent state. Three independent dimensions:
|
|
504
|
+
*
|
|
505
|
+
* analytics — track() + identify() + auto-emissions
|
|
506
|
+
* marketing — paid-traffic click IDs + referrer URL on events
|
|
507
|
+
* errors — Web Vitals + (future) error reporting
|
|
508
|
+
*
|
|
509
|
+
* Each defaults to `true` (granted). Pass partial state — only the
|
|
510
|
+
* keys you provide are changed.
|
|
511
|
+
*
|
|
512
|
+
* Crossdeck.consent({ analytics: false });
|
|
513
|
+
* Crossdeck.consent({ marketing: true, errors: true });
|
|
514
|
+
*
|
|
515
|
+
* DNT-derived denies cannot be flipped back on; if the browser said
|
|
516
|
+
* "don't track" we don't track even if the developer code disagrees.
|
|
517
|
+
*/
|
|
518
|
+
consent(state: Partial<ConsentState>): ConsentState;
|
|
519
|
+
/** Snapshot of the current consent state. */
|
|
520
|
+
consentStatus(): ConsentState;
|
|
521
|
+
/**
|
|
522
|
+
* Manually capture an error from a try/catch block.
|
|
523
|
+
*
|
|
524
|
+
* try { …risky… } catch (err) {
|
|
525
|
+
* Crossdeck.captureError(err, { context: { plan: "pro" } });
|
|
526
|
+
* }
|
|
527
|
+
*
|
|
528
|
+
* The error is shipped through the same event queue as analytics
|
|
529
|
+
* (durable, retried, rate-limited per fingerprint). Sends are gated
|
|
530
|
+
* by `consent.errors`. Returns silently — never throws, even if the
|
|
531
|
+
* SDK isn't initialised yet.
|
|
532
|
+
*/
|
|
533
|
+
captureError(error: unknown, options?: {
|
|
534
|
+
context?: Record<string, unknown>;
|
|
535
|
+
tags?: Record<string, string>;
|
|
536
|
+
level?: ErrorLevel;
|
|
537
|
+
}): void;
|
|
538
|
+
/**
|
|
539
|
+
* Capture a non-error event you want to surface as an issue
|
|
540
|
+
* ("deprecated path hit", "we entered the slow code path"). Sentry
|
|
541
|
+
* captureMessage pattern. Returns silently if not initialised.
|
|
542
|
+
*/
|
|
543
|
+
captureMessage(message: string, level?: ErrorLevel): void;
|
|
544
|
+
/**
|
|
545
|
+
* Attach a tag to every subsequent error report. Tags are key/value
|
|
546
|
+
* strings (Sentry pattern): `setTag("flow", "checkout")` → every
|
|
547
|
+
* error from this point on carries `tags.flow === "checkout"`.
|
|
548
|
+
*/
|
|
549
|
+
setTag(key: string, value: string): void;
|
|
550
|
+
/** Bulk-set tags. Merges with existing tags. */
|
|
551
|
+
setTags(tags: Record<string, string>): void;
|
|
552
|
+
/**
|
|
553
|
+
* Attach a structured context blob to every subsequent error report.
|
|
554
|
+
* Unlike tags (flat key/value), context is a named bag of arbitrary
|
|
555
|
+
* data: `setContext("cart", { items: 3, total: 42.99 })`.
|
|
556
|
+
*/
|
|
557
|
+
setContext(name: string, data: Record<string, unknown>): void;
|
|
558
|
+
/**
|
|
559
|
+
* Add a custom breadcrumb to the rolling buffer. Useful for marking
|
|
560
|
+
* domain-meaningful moments ("user opened paywall") that aren't
|
|
561
|
+
* already auto-captured. The buffer caps at 50 entries; old ones
|
|
562
|
+
* evict.
|
|
563
|
+
*/
|
|
564
|
+
addBreadcrumb(crumb: Breadcrumb): void;
|
|
565
|
+
/**
|
|
566
|
+
* Install a pre-send hook for errors. Return null to drop, or a
|
|
567
|
+
* modified CapturedError to scrub / rewrite. Sentry's beforeSend
|
|
568
|
+
* pattern — the only way to redact app-specific PII (auth tokens
|
|
569
|
+
* in URLs, etc.) before the report leaves the browser.
|
|
570
|
+
*/
|
|
571
|
+
setErrorBeforeSend(hook: ((err: CapturedError) => CapturedError | null) | null): void;
|
|
572
|
+
/**
|
|
573
|
+
* Internal: turn a CapturedError into a Crossdeck event and enqueue
|
|
574
|
+
* it. Goes through the same queue / persistence / consent / scrub
|
|
575
|
+
* pipeline as analytics events.
|
|
576
|
+
*/
|
|
577
|
+
private reportError;
|
|
578
|
+
/**
|
|
579
|
+
* GDPR/CCPA "right to be forgotten" — calls the backend's
|
|
580
|
+
* /v1/identity/forget endpoint to schedule a server-side deletion of
|
|
581
|
+
* the customer's events and profile, then wipes all local state
|
|
582
|
+
* (identity, entitlements, queue, super-props, persistent stores).
|
|
583
|
+
*
|
|
584
|
+
* Idempotent. Safe to call when no identity has been established
|
|
585
|
+
* (it just wipes the empty local state).
|
|
586
|
+
*
|
|
587
|
+
* After forget() resolves, the SDK is in the same shape as if the
|
|
588
|
+
* developer had called `Crossdeck.reset()` — a fresh anonymousId is
|
|
589
|
+
* minted and the next session is a brand new identity-graph entry.
|
|
590
|
+
*/
|
|
591
|
+
forget(): Promise<void>;
|
|
592
|
+
/**
|
|
593
|
+
* Read the current customer's active entitlements from the server.
|
|
594
|
+
* Updates the local cache so subsequent isEntitled() calls answer
|
|
595
|
+
* synchronously.
|
|
596
|
+
*/
|
|
597
|
+
getEntitlements(): Promise<PublicEntitlement[]>;
|
|
598
|
+
/**
|
|
599
|
+
* Synchronous read from the durable local cache — answers from
|
|
600
|
+
* last-known-good. The cache hydrates from device storage on boot and
|
|
601
|
+
* survives a Crossdeck outage, so a returning paying customer reads
|
|
602
|
+
* true even before the session's first network round-trip. Returns
|
|
603
|
+
* false only for a genuinely new install that has never completed a
|
|
604
|
+
* getEntitlements(), or for an entitlement past its own validUntil.
|
|
605
|
+
*/
|
|
606
|
+
isEntitled(key: string): boolean;
|
|
607
|
+
/** Snapshot of the local entitlement cache. */
|
|
608
|
+
listEntitlements(): PublicEntitlement[];
|
|
609
|
+
/**
|
|
610
|
+
* Subscribe to entitlement-cache changes. Returns an unsubscribe fn.
|
|
611
|
+
*
|
|
612
|
+
* The listener is invoked AFTER the cache mutates — once after a
|
|
613
|
+
* successful `getEntitlements()` warms it, again after `syncPurchases()`
|
|
614
|
+
* delivers fresh entitlements, and once on `reset()` to fire the
|
|
615
|
+
* empty-cache state for logout flows.
|
|
616
|
+
*
|
|
617
|
+
* It is NOT invoked synchronously on subscribe. Callers that need
|
|
618
|
+
* the current state should read it via `isEntitled()` / `listEntitlements()`
|
|
619
|
+
* inline; the listener fires only on FUTURE changes.
|
|
620
|
+
*
|
|
621
|
+
* This is the foundation of the `useEntitlement` React hook in
|
|
622
|
+
* `@cross-deck/web/react` — without it, React (or SwiftUI / Compose
|
|
623
|
+
* / Vue) would have no way to re-render when entitlements arrive
|
|
624
|
+
* asynchronously after init. The naive pattern of calling
|
|
625
|
+
* `Crossdeck.isEntitled("pro")` directly inside a render path
|
|
626
|
+
* shows the empty-cache result forever; binding the result to
|
|
627
|
+
* component state via `onEntitlementsChange` is the correct
|
|
628
|
+
* pattern.
|
|
629
|
+
*
|
|
630
|
+
* Idempotent unsubscribe — calling the returned function multiple
|
|
631
|
+
* times is safe.
|
|
632
|
+
*
|
|
633
|
+
* Listener errors are swallowed (a buggy listener can't crash the
|
|
634
|
+
* SDK or other listeners).
|
|
635
|
+
*/
|
|
636
|
+
onEntitlementsChange(listener: EntitlementsListener): () => void;
|
|
637
|
+
/**
|
|
638
|
+
* Queue a telemetry event. Returns immediately — the network round-
|
|
639
|
+
* trip happens in the background. To flush before the page unloads,
|
|
640
|
+
* call flush().
|
|
641
|
+
*/
|
|
642
|
+
/**
|
|
643
|
+
* Emit `crossdeck.contract_failed` with the canonical property
|
|
644
|
+
* shape (`contract_id`, `sdk_version`, `sdk_platform`,
|
|
645
|
+
* `failure_reason`, `run_context`, `run_id`). Goes through the
|
|
646
|
+
* standard track() pipeline — same consent gate, same queue,
|
|
647
|
+
* same ingest, no new endpoint.
|
|
648
|
+
*
|
|
649
|
+
* Wire the call from a test hook, dogfood failure path, or
|
|
650
|
+
* customer contract-verification harness; see
|
|
651
|
+
* `contracts/README.md` for the per-test-framework hook recipes.
|
|
652
|
+
*/
|
|
653
|
+
reportContractFailure(input: ContractFailureInput): void;
|
|
654
|
+
track(name: string, properties?: EventProperties): void;
|
|
655
|
+
/**
|
|
656
|
+
* Force-flush queued events. Useful to call from page-unload handlers.
|
|
657
|
+
*
|
|
658
|
+
* Pass `{ keepalive: true }` from terminal handlers (pagehide /
|
|
659
|
+
* visibilitychange→hidden / beforeunload). The browser keeps the
|
|
660
|
+
* request alive after the page tears down, so the final batch
|
|
661
|
+
* actually lands instead of being cancelled with the unload.
|
|
662
|
+
*
|
|
663
|
+
* NorthStar §4: standard method name across all Crossdeck SDKs.
|
|
664
|
+
*/
|
|
665
|
+
flush(options?: {
|
|
666
|
+
keepalive?: boolean;
|
|
667
|
+
}): Promise<void>;
|
|
668
|
+
/** @deprecated Use `flush()` instead. NorthStar §4 standardised the name. */
|
|
669
|
+
flushEvents(): Promise<void>;
|
|
670
|
+
/**
|
|
671
|
+
* Forward purchase evidence to the backend for verification + entitlement
|
|
672
|
+
* projection. NorthStar §4 + §13 canonical name.
|
|
673
|
+
*
|
|
674
|
+
* Today the web SDK only supports Apple StoreKit 2 forwarding (web apps
|
|
675
|
+
* that sit alongside an iOS app). Stripe doesn't need this method —
|
|
676
|
+
* Stripe webhooks deliver evidence server-side without a client round-trip.
|
|
677
|
+
*/
|
|
678
|
+
syncPurchases(input: {
|
|
679
|
+
rail?: "apple";
|
|
680
|
+
signedTransactionInfo: string;
|
|
681
|
+
signedRenewalInfo?: string;
|
|
682
|
+
appAccountToken?: string;
|
|
683
|
+
}): Promise<PurchaseResult>;
|
|
684
|
+
/** @deprecated Use `syncPurchases()` instead. NorthStar §4 standardised the name. */
|
|
685
|
+
purchaseApple(input: {
|
|
686
|
+
signedTransactionInfo: string;
|
|
687
|
+
signedRenewalInfo?: string;
|
|
688
|
+
appAccountToken?: string;
|
|
689
|
+
}): Promise<PurchaseResult>;
|
|
690
|
+
/**
|
|
691
|
+
* Toggle verbose diagnostic logging — NorthStar §16. When enabled, the
|
|
692
|
+
* SDK emits a fixed vocabulary of debug signals to console.info that the
|
|
693
|
+
* dashboard's onboarding checklist can also surface as live events.
|
|
694
|
+
*/
|
|
695
|
+
setDebugMode(enabled: boolean): void;
|
|
696
|
+
/**
|
|
697
|
+
* Send the boot heartbeat. Called automatically by start() unless
|
|
698
|
+
* autoHeartbeat:false. Safe to call manually as a "we're still here" ping.
|
|
699
|
+
*/
|
|
700
|
+
heartbeat(): Promise<HeartbeatResponse>;
|
|
701
|
+
/**
|
|
702
|
+
* Wipe persisted identity + entitlement cache. Use on logout. The
|
|
703
|
+
* next pre-login session generates a fresh anonymousId and starts a
|
|
704
|
+
* new identity-graph entry.
|
|
705
|
+
*/
|
|
706
|
+
reset(): void;
|
|
707
|
+
/**
|
|
708
|
+
* Diagnostic: current state + queue stats. Useful for the dashboard's
|
|
709
|
+
* heartbeat row and debugging in dev.
|
|
710
|
+
*
|
|
711
|
+
* Returns a stable shape regardless of whether start() has been called —
|
|
712
|
+
* callers don't need to narrow on `started` to access `events` or
|
|
713
|
+
* `entitlements`. Pre-start values are sensible empties.
|
|
714
|
+
*/
|
|
715
|
+
diagnostics(): Diagnostics;
|
|
716
|
+
private requireStarted;
|
|
717
|
+
/**
|
|
718
|
+
* Build the identity query for /v1/entitlements. Priority:
|
|
719
|
+
* crossdeckCustomerId > developerUserId > anonymousId
|
|
720
|
+
* — matches the resolveCrossdeckCustomerId precedence on the server.
|
|
721
|
+
*/
|
|
722
|
+
private identityQueryParams;
|
|
723
|
+
/**
|
|
724
|
+
* Embed every known identity axis on the event. Earlier this returned
|
|
725
|
+
* just the highest-priority hint (cdcust → developerUserId → anonymousId)
|
|
726
|
+
* to keep payloads small, but that leaked into analytics: once a user
|
|
727
|
+
* was logged in, every subsequent page.viewed shipped without
|
|
728
|
+
* anonymousId, and `uniqExact(anonymous_id)` on the warehouse side
|
|
729
|
+
* counted 0 visitors for the entire authenticated app.
|
|
730
|
+
*
|
|
731
|
+
* Bank-grade rule: the server is the single source of truth on
|
|
732
|
+
* dedup. Send everything we know; let CH count by whichever axis
|
|
733
|
+
* matches the question. Each field is at most 32 bytes — sending
|
|
734
|
+
* three on every event costs ~80 bytes per request, which is
|
|
735
|
+
* trivial compared to the analytics correctness it buys.
|
|
736
|
+
*/
|
|
737
|
+
private identityHintForEvent;
|
|
738
|
+
private mintEventId;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Default singleton — most consumers want one SDK instance per app.
|
|
742
|
+
* Creating extra instances is fine; just `new CrossdeckClient()`.
|
|
743
|
+
*/
|
|
744
|
+
declare const Crossdeck: CrossdeckClient;
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Stripe-style error wrapper for @cross-deck/web.
|
|
748
|
+
*
|
|
749
|
+
* Mirrors the wire shape returned by the v1 backend (see
|
|
750
|
+
* backend/src/api/v1-errors.ts) so SDK consumers can `catch`
|
|
751
|
+
* with consistent fields:
|
|
752
|
+
*
|
|
753
|
+
* try {
|
|
754
|
+
* await crossdeck.identify("user_847");
|
|
755
|
+
* } catch (err) {
|
|
756
|
+
* if (err instanceof CrossdeckError && err.code === "invalid_api_key") {
|
|
757
|
+
* // ...
|
|
758
|
+
* }
|
|
759
|
+
* }
|
|
760
|
+
*/
|
|
761
|
+
type CrossdeckErrorType = "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "internal_error" | "network_error" | "configuration_error";
|
|
762
|
+
interface CrossdeckErrorPayload {
|
|
763
|
+
type: CrossdeckErrorType;
|
|
764
|
+
code: string;
|
|
765
|
+
message: string;
|
|
766
|
+
/** Server-issued request ID. Echoed in support tickets. */
|
|
767
|
+
requestId?: string;
|
|
768
|
+
/** HTTP status code if the error came from an API response. */
|
|
769
|
+
status?: number;
|
|
770
|
+
/**
|
|
771
|
+
* Server-suggested wait (in milliseconds) before retrying. Populated
|
|
772
|
+
* from the `Retry-After` response header on 429 / 503. The header
|
|
773
|
+
* spec allows either delta-seconds or an HTTP-date; the parser below
|
|
774
|
+
* normalises both to milliseconds. Consumers MUST honour this — the
|
|
775
|
+
* server is telling you the safe rate.
|
|
776
|
+
*/
|
|
777
|
+
retryAfterMs?: number;
|
|
778
|
+
}
|
|
779
|
+
declare class CrossdeckError extends Error {
|
|
780
|
+
readonly type: CrossdeckErrorType;
|
|
781
|
+
readonly code: string;
|
|
782
|
+
readonly requestId?: string;
|
|
783
|
+
readonly status?: number;
|
|
784
|
+
readonly retryAfterMs?: number;
|
|
785
|
+
constructor(payload: CrossdeckErrorPayload);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Storage adapters for SDK-persisted state.
|
|
790
|
+
*
|
|
791
|
+
* Three flavours:
|
|
792
|
+
* - browser localStorage (default in browsers)
|
|
793
|
+
* - 1st-party document.cookie (redundancy for cleared localStorage)
|
|
794
|
+
* - in-memory (default in Node, or as an explicit fallback)
|
|
795
|
+
*
|
|
796
|
+
* Detection is at construction time, not at every call — picking the
|
|
797
|
+
* adapter once means we don't hit `typeof window` checks on hot paths.
|
|
798
|
+
*
|
|
799
|
+
* ----- Bank-grade identity continuity -----
|
|
800
|
+
*
|
|
801
|
+
* Plain localStorage is not enough. ITP, private browsing, "clear site
|
|
802
|
+
* data" actions, and aggressive privacy extensions all wipe it. When
|
|
803
|
+
* that happens, the SDK mints a fresh anonymousId on next page load
|
|
804
|
+
* and the customer's analytics see one human as multiple "new
|
|
805
|
+
* visitors" — a credibility hit on every dashboard chart that depends
|
|
806
|
+
* on visitor uniqueness (new vs returning, retention, funnels).
|
|
807
|
+
*
|
|
808
|
+
* The fix is redundancy: we write the same identity to BOTH
|
|
809
|
+
* localStorage AND a 1st-party cookie. On boot we read both; whichever
|
|
810
|
+
* survived wins. On set, we write to both stores so a future clear of
|
|
811
|
+
* either doesn't lose the user.
|
|
812
|
+
*
|
|
813
|
+
* Caveats (documented honestly):
|
|
814
|
+
* 1. Safari ITP caps client-set 1st-party cookies at 7 days. Cookie
|
|
815
|
+
* redundancy protects against localStorage clears WITHIN that
|
|
816
|
+
* 7-day window, not beyond it. The full ITP-bypass story (server-
|
|
817
|
+
* set cookies via a customer-CNAMEd subdomain) is a Phase 2
|
|
818
|
+
* follow-up that requires customer DNS configuration.
|
|
819
|
+
* 2. We never write fingerprintable data — only the same anonymousId
|
|
820
|
+
* already in localStorage. Privacy posture is unchanged from
|
|
821
|
+
* single-store identity.
|
|
822
|
+
* 3. `persistIdentity: false` disables BOTH stores so customers
|
|
823
|
+
* running strict consent flows can defer cookie writes until the
|
|
824
|
+
* user opts in.
|
|
825
|
+
*/
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* In-memory storage. Cleared on process exit. Useful for Node runtimes
|
|
829
|
+
* where you want session-scoped identity that doesn't persist to disk.
|
|
830
|
+
*/
|
|
831
|
+
declare class MemoryStorage implements KeyValueStorage {
|
|
832
|
+
private store;
|
|
833
|
+
getItem(key: string): string | null;
|
|
834
|
+
setItem(key: string, value: string): void;
|
|
835
|
+
removeItem(key: string): void;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* SDK version constant — generated by `scripts/sync-sdk-versions.mjs`.
|
|
840
|
+
*
|
|
841
|
+
* Single source of truth: the `version` field in this package's
|
|
842
|
+
* package.json. The sync script writes this file so that
|
|
843
|
+
* `SDK_VERSION` is a plain TypeScript literal at runtime — no
|
|
844
|
+
* runtime JSON-import gotcha (Node ESM requires
|
|
845
|
+
* `with { type: "json" }` to import JSON as ESM, and the published
|
|
846
|
+
* dist file would otherwise fail to load).
|
|
847
|
+
*
|
|
848
|
+
* Drift protection: `node scripts/sync-sdk-versions.mjs --check` (the
|
|
849
|
+
* CI gate) flags this file when it falls out of sync with package.json.
|
|
850
|
+
* Bumping `package.json` without re-running the sync script fails CI.
|
|
851
|
+
*
|
|
852
|
+
* Do NOT edit by hand — `node scripts/sync-sdk-versions.mjs`.
|
|
853
|
+
*/
|
|
854
|
+
declare const SDK_VERSION = "1.4.2";
|
|
855
|
+
declare const SDK_NAME = "@cross-deck/web";
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* HTTP transport for the SDK. Single fetch wrapper used by every endpoint
|
|
859
|
+
* call. Adds the Bearer token and SDK version header, parses responses,
|
|
860
|
+
* normalises errors to CrossdeckError.
|
|
861
|
+
*
|
|
862
|
+
* Uses platform-native fetch (browser + Node 18+). No axios, no isomorphic-
|
|
863
|
+
* fetch shim, no transitive deps.
|
|
864
|
+
*/
|
|
865
|
+
|
|
866
|
+
declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Machine-readable index of every error code the SDK can throw, with
|
|
870
|
+
* a short description and a hint on what action to take. Published
|
|
871
|
+
* verbatim as `crossdeck-error-codes.json` in the npm tarball so AI
|
|
872
|
+
* integration assistants, error-aggregator dashboards (Sentry,
|
|
873
|
+
* DataDog), and the Crossdeck dashboard can render human-friendly
|
|
874
|
+
* messages without parsing freeform `message` strings.
|
|
875
|
+
*
|
|
876
|
+
* Stripe publishes the same surface at stripe.com/docs/error-codes;
|
|
877
|
+
* developers love it because every code has a canonical "what does
|
|
878
|
+
* this mean / what should I do" answer.
|
|
879
|
+
*
|
|
880
|
+
* Adding a new error code:
|
|
881
|
+
* 1. Add the code string to the union in `errors.ts` (where used).
|
|
882
|
+
* 2. Add an entry here.
|
|
883
|
+
* 3. The next `npm run build` regenerates the JSON sidecar.
|
|
884
|
+
*
|
|
885
|
+
* Keep entries terse — the consumer surfaces this in tooltips and
|
|
886
|
+
* automated tickets, not in long-form docs.
|
|
887
|
+
*/
|
|
888
|
+
interface ErrorCodeEntry {
|
|
889
|
+
/** The string thrown as CrossdeckError.code. */
|
|
890
|
+
code: string;
|
|
891
|
+
/** CrossdeckError.type — broad category. */
|
|
892
|
+
type: "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "internal_error" | "network_error" | "configuration_error";
|
|
893
|
+
/** One-sentence description. Surfaced verbatim in dashboards. */
|
|
894
|
+
description: string;
|
|
895
|
+
/** What the developer should do. Imperative phrasing. */
|
|
896
|
+
resolution: string;
|
|
897
|
+
/** True for codes the SDK can auto-recover from (no developer action). */
|
|
898
|
+
retryable: boolean;
|
|
899
|
+
}
|
|
900
|
+
declare const CROSSDECK_ERROR_CODES: readonly ErrorCodeEntry[];
|
|
901
|
+
/** Lookup helper — returns the entry matching a CrossdeckError.code, or undefined. */
|
|
902
|
+
declare function getErrorCode(code: string): ErrorCodeEntry | undefined;
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Device + environment enrichment.
|
|
906
|
+
*
|
|
907
|
+
* Auto-attached to every event the SDK emits when `autoTrack.deviceInfo` is
|
|
908
|
+
* enabled (default). Caller-supplied event properties always override
|
|
909
|
+
* auto-detected ones (so a developer can manually set `app.version` per
|
|
910
|
+
* event if they want to A/B between builds).
|
|
911
|
+
*
|
|
912
|
+
* Privacy posture:
|
|
913
|
+
* - No fingerprinting (no canvas hashes, no font enumeration).
|
|
914
|
+
* - No precise geolocation (only timezone + locale, both of which the
|
|
915
|
+
* browser exposes to every page anyway).
|
|
916
|
+
* - No IP collection — the backend logs the request IP for rate-limit
|
|
917
|
+
* purposes; it isn't stored on the event document.
|
|
918
|
+
* - All fields are typed enums or short strings; we never echo back
|
|
919
|
+
* full User-Agent strings to avoid surfacing fingerprintable detail
|
|
920
|
+
* in dashboards.
|
|
921
|
+
*/
|
|
922
|
+
interface DeviceInfo {
|
|
923
|
+
os?: string;
|
|
924
|
+
osVersion?: string;
|
|
925
|
+
browser?: string;
|
|
926
|
+
browserVersion?: string;
|
|
927
|
+
locale?: string;
|
|
928
|
+
timezone?: string;
|
|
929
|
+
screenWidth?: number;
|
|
930
|
+
screenHeight?: number;
|
|
931
|
+
viewportWidth?: number;
|
|
932
|
+
viewportHeight?: number;
|
|
933
|
+
devicePixelRatio?: number;
|
|
934
|
+
/** Caller-supplied. Set via Crossdeck.start({ appVersion: "1.2.3" }). */
|
|
935
|
+
appVersion?: string;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
export { AliasResult, type Breadcrumb, type BreadcrumbCategory, type BreadcrumbLevel, CROSSDECK_ERROR_CODES, type CapturedError, type ConsentState, type Contract, type ContractAppliesTo, type ContractFailureInput, type ContractPillar, type ContractStatus, type ContractTestRef, Crossdeck, CrossdeckClient, CrossdeckContracts, CrossdeckError, type CrossdeckErrorPayload, type CrossdeckErrorType, CrossdeckOptions, DEFAULT_BASE_URL, type DeviceInfo, Diagnostics, type ErrorCodeEntry, type ErrorLevel, EventProperties, GroupTraits, HeartbeatResponse, IdentifyOptions, KeyValueStorage, MemoryStorage, PublicEntitlement, PurchaseResult, SDK_NAME, SDK_VERSION, type StackFrame, getErrorCode };
|