@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/CHANGELOG.md +93 -0
- package/dist/crossdeck.umd.min.js +2 -0
- package/dist/crossdeck.umd.min.js.map +1 -0
- package/dist/error-codes.json +91 -0
- package/dist/index.cjs +1403 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +281 -5
- package/dist/index.d.ts +281 -5
- package/dist/index.mjs +1400 -38
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +1303 -37
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +1303 -37
- package/dist/react.mjs.map +1 -1
- package/dist/vue.cjs +2675 -0
- package/dist/vue.cjs.map +1 -0
- package/dist/vue.d.mts +37 -0
- package/dist/vue.d.ts +37 -0
- package/dist/vue.mjs +2649 -0
- package/dist/vue.mjs.map +1 -0
- package/package.json +25 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,99 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@cross-deck/web` will be documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
4
|
|
|
5
|
+
## [0.10.0] — 2026-05-11
|
|
6
|
+
|
|
7
|
+
**Privacy + compliance + operational pass (Waves 3 + 4).** Locks down GDPR / CCPA support, ships the CDN + framework story, and publishes the error-code surface that Stripe-style integrators depend on. Backwards-compatible — every new field defaults to "don't change behaviour". Source-compatible with 0.9.x.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`Crossdeck.consent({ analytics, marketing, errors })`** — three independent consent dimensions, each defaulting to `true` (granted). Gates `track()`, `identify()`, paid-traffic click IDs, referrer URLs, and Web Vitals appropriately. `Crossdeck.consentStatus()` returns the current snapshot.
|
|
12
|
+
- **`respectDnt: true`** in `init()` — opt-in DNT support. When the browser exposes `navigator.doNotTrack === "1"`, ALL three consent dimensions are locked OFF permanently (no subsequent `consent()` call can flip them back on).
|
|
13
|
+
- **`scrubPii: true`** (default-on) in `init()` — Stripe-grade regex pass over every event property value, URL path, and title before flush. Email-shaped → `[email]`, card-number-shaped → `[card]`. Caller's input is never mutated. Disable for pipelines that do their own redaction.
|
|
14
|
+
- **`Crossdeck.forget(): Promise<void>`** — GDPR / CCPA right to be forgotten. Calls the new `/v1/identity/forget` endpoint and wipes ALL local state. Idempotent. Server-side failure does NOT block local wipe.
|
|
15
|
+
- **`@cross-deck/web/vue` subpackage** — Vue 3 composables (`useEntitlement(key)` → `Ref<boolean>`, `useEntitlements()` → `Ref<string[]>`) that mirror the React subpackage's contract. Subscribes to the entitlement cache via `onEntitlementsChange`. SSR-safe.
|
|
16
|
+
- **UMD CDN bundle** — `dist/crossdeck.umd.min.js`, registered via `unpkg` / `jsdelivr` package.json fields. Exposes `window.Crossdeck` for no-build-step consumers (plain HTML, Webflow, docs). 13 KB gzipped.
|
|
17
|
+
- **`CROSSDECK_ERROR_CODES` + `getErrorCode(code)`** — machine-readable index of every error code the SDK can throw, with `description`, `resolution`, and `retryable` flag. Also emitted as `dist/error-codes.json` sidecar. Stripe pattern.
|
|
18
|
+
- **Bundle-size budget enforcement** — `npm run size` (also runs in `prepublishOnly`) fails the release if any artefact exceeds its gzipped budget. Current ceilings: 28 KB for core / framework subpackages, 16 KB for UMD.
|
|
19
|
+
- **New debug signals:** `sdk.consent_changed`, `sdk.consent_denied`, `sdk.consent_dnt_applied`, `sdk.pii_scrubbed`.
|
|
20
|
+
|
|
21
|
+
### Backend changes (paired)
|
|
22
|
+
|
|
23
|
+
- **`POST /v1/identity/forget`** — new endpoint. Resolves the customer from any identity hint, sets `forgottenAt: now` on the customer record, queues a `forgetRequests` row for the retention-cleanup worker to drain.
|
|
24
|
+
- **`POST /v1/identity/alias`** — now accepts optional `traits` in the body and persists them under `customers/{cdcust}.traits` additively (per-key merge). Defence-in-depth sanitisation server-side: max 32 keys, 1 KB per value, primitives only.
|
|
25
|
+
|
|
26
|
+
### Compatibility
|
|
27
|
+
|
|
28
|
+
Source-compatible with 0.9.x. The new defaults (`scrubPii: true`, `respectDnt: false`) preserve existing analytics shape for current consumers. The Vue subpackage adds an optional peer dependency declared in `peerDependenciesMeta` — non-Vue consumers don't install it.
|
|
29
|
+
|
|
30
|
+
## [0.9.0] — 2026-05-11
|
|
31
|
+
|
|
32
|
+
**Data completeness pass (Wave 2).** Closes the gap between Crossdeck's event surface and Mixpanel / Segment / Amplitude. Backwards-compatible — `Crossdeck.init({...})` callsites don't need to change; the new APIs are additive.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **`Crossdeck.identify(userId, { traits })`** — accept profile traits (name, plan, signupDate, role) alongside the email field. Traits are sanitised at the SDK boundary and persisted server-side on the customer record under `customers/{cdcust}.traits` (per-key merge, additive — a later identify call with `{ plan: "pro" }` doesn't wipe a prior call's `{ name: "Wes" }`). Defence-in-depth server validation: max 32 keys, 1 KB per value, primitives only.
|
|
37
|
+
- **`Crossdeck.register(properties)` + `unregister(key)` + `getSuperProperties()`** — Mixpanel "super properties" pattern. Set keys once, attached to every subsequent event of this SDK instance. Null value deletes a key. Persists across page reloads via the identity storage; cleared on `reset()` / `forget()`.
|
|
38
|
+
- **`Crossdeck.group(type, id, traits?)` + `getGroups()`** — Mixpanel / Segment "Group Analytics". Each event carries `$groups.<type>: id` for B2B SaaS dashboards. Multiple types coexist (`org` + `team` + `plan`). Pass `id: null` to clear a group membership.
|
|
39
|
+
- **Paid-traffic click ID capture** — `gclid` (Google Ads), `fbclid` (Meta), `msclkid` (Microsoft), `ttclid` (TikTok), `li_fat_id` (LinkedIn), `twclid` (X / Twitter). Captured at session start alongside UTMs, attached to every event of the session.
|
|
40
|
+
- **`pageviewId`** — stable per-page-view identifier minted on every `page.viewed` and attached to every subsequent event until the next `page.viewed`. Mixpanel's `$current_url`-style correlation — lets dashboards answer "user clicked X on page Y" without timestamp arithmetic.
|
|
41
|
+
- **Web Vitals capture** — `webvitals.lcp`, `webvitals.inp`, `webvitals.cls`, `webvitals.fcp`, `webvitals.ttfb` events emitted via `PerformanceObserver`. LCP / CLS / INP flush at page hidden (final values only known after user activity stops). New `autoTrack.webVitals` flag (default true). Hand-rolled (~120 lines), zero runtime deps.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- `IdentifyOptions` extended with `traits?: Record<string, unknown>`. Existing `email`-only callers unaffected.
|
|
46
|
+
- `AutoTrackOptions` extended with `webVitals: boolean`. Existing `init()` callsites without `autoTrack` get it default-on.
|
|
47
|
+
- `SessionAcquisition` extended with the six paid-traffic click-ID fields. Existing acquisition consumers unaffected — fields are empty strings when not present.
|
|
48
|
+
|
|
49
|
+
### Compatibility
|
|
50
|
+
|
|
51
|
+
Source-compatible with 0.8.x. No public API removed. Every new field defaults to a sensible value that preserves the previous behaviour for existing callsites.
|
|
52
|
+
|
|
53
|
+
## [0.8.0] — 2026-05-11
|
|
54
|
+
|
|
55
|
+
**Bank-grade plumbing pass (Wave 1).** Six closely-coupled hardenings that bring the SDK's reliability surface up to Stripe / Segment / Mixpanel standards. Backwards-compatible: no public API removed, every new option has a sensible default, every behaviour change is additive. Source-compatible with 0.7.x — `Crossdeck.init({...})` callsites do not need to change.
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- **Durable event queue.** Queued events are now written through to the SDK's identity store (typically `localStorage`) so a hard browser crash, power loss, or terminal-flush `keepalive: true` cap exceedance (64 KB) doesn't lose data. On the next SDK boot the persisted queue is rehydrated and replayed. Backend dedupes by `eventId` so a replayed event already on the wire when the tab crashed is safe — `ReplacingMergeTree` handles it. New module `event-storage.ts` (`PersistentEventStore`). Skipped when `persistIdentity: false` (strict-consent flows).
|
|
60
|
+
- **Exponential backoff with full jitter on flush failures.** Replaces the prior "retry on the next idle window" policy which hot-looped a flapping endpoint. Defaults: `baseMs=1000`, `factor=2`, `maxMs=60000`. Each failure schedules the next flush at `min(maxMs, baseMs * 2^attempts) * Math.random()` ms out. Reset on success. Surface via `diagnostics().events.consecutiveFailures` + `nextRetryAt`. New module `retry-policy.ts` (`RetryPolicy`, `computeNextDelay`).
|
|
61
|
+
- **`Retry-After` header support on 429 / 503.** The HTTP layer now parses the header (delta-seconds or HTTP-date per RFC 7231 §7.1.3) onto `CrossdeckError.retryAfterMs`, and the retry policy honours it when it's longer than the computed backoff. Stripe pattern — the server is the authority on its own pressure.
|
|
62
|
+
- **`Idempotency-Key` header per batch.** Every `/v1/events` POST now carries `Idempotency-Key: batch_<rand>`. Retries of the SAME logical batch reuse the SAME key so a future server-side idempotency layer can short-circuit duplicate work without inspecting bodies. Per-event `eventId` dedup remains in place — this is belt-and-suspenders.
|
|
63
|
+
- **Request timeout via `AbortController`.** New `timeoutMs` option on `CrossdeckOptions` and per-request `options.timeoutMs` on `HttpClient.request()`. Default 15 000 ms. Without this, a captive portal / DNS hang / satellite link could leave a request open for the browser's default (5+ minutes on Chrome) and lock the queue forever. Pass `timeoutMs: 0` to disable (useful for tests). New error: `CrossdeckError({ type: "network_error", code: "request_timeout" })`.
|
|
64
|
+
- **Property validation at enqueue.** `track(name, properties)` now sanitises `properties` BEFORE the event lands in the queue. New module `event-validation.ts`. Behaviour:
|
|
65
|
+
- **Drops** functions, symbols, undefined values (with a debug warning).
|
|
66
|
+
- **Coerces** `Date` → ISO string, `BigInt` → string, `Error` → `{ name, message, stack }`, `Map` → plain object, `Set` → array.
|
|
67
|
+
- **Truncates** string values longer than `maxStringLength` (default 1024) with an ellipsis.
|
|
68
|
+
- **Replaces** circular refs with `"[circular]"` and depth > 5 nesting with `"[depth-exceeded]"`.
|
|
69
|
+
- **Caps** total per-event property byte size at `maxBatchPropertyBytes` (default 8 KB); past the cap, largest properties drop first and a `__truncated: true` marker is added.
|
|
70
|
+
- Caller's input is never mutated — sanitisation always produces a defensive copy.
|
|
71
|
+
- Output is guaranteed `JSON.stringify`-safe. One bad property can no longer poison the entire batch indefinitely.
|
|
72
|
+
- **Listener-error counter on `EntitlementCache`.** Listener exceptions are still swallowed (a buggy consumer must not crash the SDK) but the cumulative count is now surfaced as `diagnostics().entitlements.listenerErrors` so a broken subscriber can be spotted without a debug session.
|
|
73
|
+
- **Clock-skew diagnostics.** `Crossdeck.heartbeat()` now captures the server's `serverTime` and the local `Date.now()` at the same moment. Surfaces via `diagnostics().clock.{lastServerTime, lastClientTime, skewMs}` so a wrong-system-clock problem (kid changed the date, dev machine bad NTP) surfaces in dashboards before it corrupts a day of analytics.
|
|
74
|
+
- **New debug signals:** `sdk.property_coerced`, `sdk.queue_persisted`, `sdk.queue_restored`, `sdk.flush_retry_scheduled`. Fire in debug mode only — quiet by default.
|
|
75
|
+
- **65 new tests** (203 total, up from 138):
|
|
76
|
+
- `tests/event-validation.test.ts` — 19 cases covering every coercion / drop / truncation / depth / size-cap path + JSON-roundtrip + no-mutation guarantee.
|
|
77
|
+
- `tests/event-storage.test.ts` — 8 cases covering load / save round-trip, debouncing, malformed-blob recovery, version sentinel, throwing-storage degradation.
|
|
78
|
+
- `tests/retry-policy.test.ts` — 12 cases covering backoff math, jitter, Retry-After precedence, attempt overflow safety, counter reset.
|
|
79
|
+
- `tests/event-queue.test.ts` — 9 new cases covering Idempotency-Key uniqueness, retry scheduling, server Retry-After honouring, durable rehydration, write-through, persistent clear on success, reset() wipe.
|
|
80
|
+
- `tests/http.test.ts` — 5 new cases covering Idempotency-Key passthrough, abort-timeout behaviour, per-call timeout override, 0-disables-timeout, Retry-After parse onto `retryAfterMs`.
|
|
81
|
+
- `tests/errors.test.ts` — 9 new cases covering `parseRetryAfterHeader` for delta-seconds, HTTP-date, past dates, malformed input.
|
|
82
|
+
- `tests/entitlement-cache.test.ts` — 1 new case covering the listener-error counter.
|
|
83
|
+
- `tests/crossdeck.test.ts` — 1 new case asserting the full Wave-1 diagnostic surface.
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
|
|
87
|
+
- `CrossdeckError` now carries an optional `retryAfterMs` field, populated from the response's `Retry-After` header on 4xx/5xx.
|
|
88
|
+
- `Diagnostics` shape extended with:
|
|
89
|
+
- `clock: { lastServerTime, lastClientTime, skewMs }`
|
|
90
|
+
- `entitlements.listenerErrors: number`
|
|
91
|
+
- `events.consecutiveFailures: number`, `events.nextRetryAt: number | null`
|
|
92
|
+
- Existing `Diagnostics` fields and their semantics are unchanged.
|
|
93
|
+
|
|
94
|
+
### Migration
|
|
95
|
+
|
|
96
|
+
No callsite changes required. New options (`timeoutMs`, retry tuning) default to sensible bank-grade values. To opt out of property validation, pass already-clean property objects — there's no escape hatch, and there shouldn't be: an SDK that lets one bad event poison the whole batch isn't bank-grade.
|
|
97
|
+
|
|
5
98
|
## [0.6.0] — 2026-05-10
|
|
6
99
|
|
|
7
100
|
Bank-grade analytics enrichment. Two additive changes that close the gap between Crossdeck's analytics surface and Google Analytics 4 / Google Ads dashboards: identity continuity that survives cleared storage, and first-touch acquisition attribution attached to every event of a session. No public API changes — `Crossdeck.init({...})` callsites do not need to change.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var Crossdeck=(()=>{var X=Object.defineProperty;var be=Object.getOwnPropertyDescriptor;var ke=Object.getOwnPropertyNames;var we=Object.prototype.hasOwnProperty;var _e=(r,e)=>{for(var t in e)X(r,t,{get:e[t],enumerable:!0})},Se=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of ke(e))!we.call(r,s)&&s!==t&&X(r,s,{get:()=>e[s],enumerable:!(n=be(e,s))||n.enumerable});return r};var Ee=r=>Se(X({},"__esModule",{value:!0}),r);var tt={};_e(tt,{CROSSDECK_ERROR_CODES:()=>ie,Crossdeck:()=>ge,CrossdeckClient:()=>F,CrossdeckError:()=>g,DEFAULT_BASE_URL:()=>U,MemoryStorage:()=>w,SDK_NAME:()=>C,SDK_VERSION:()=>q,getErrorCode:()=>ve});var g=class r extends Error{constructor(e){super(e.message),this.name="CrossdeckError",this.type=e.type,this.code=e.code,this.requestId=e.requestId,this.status=e.status,this.retryAfterMs=e.retryAfterMs,Object.setPrototypeOf(this,r.prototype)}};async function oe(r){let e=r.headers.get("x-request-id")??void 0,t=Ie(r.headers.get("retry-after")),n;try{n=await r.json()}catch{n=null}let s=n?.error;return s&&typeof s.type=="string"&&typeof s.code=="string"?new g({type:s.type,code:s.code,message:s.message??`HTTP ${r.status}`,requestId:s.request_id??e,status:r.status,retryAfterMs:t}):new g({type:Te(r.status),code:`http_${r.status}`,message:`HTTP ${r.status} ${r.statusText||""}`.trim(),requestId:e,status:r.status,retryAfterMs:t})}function Ie(r){if(!r)return;let e=r.trim();if(!e)return;if(/^\d+(\.\d+)?$/.test(e)){let s=Number(e);return!Number.isFinite(s)||s<0?void 0:Math.round(s*1e3)}if(!/[a-zA-Z,/:]/.test(e))return;let t=Date.parse(e);if(!Number.isFinite(t))return;let n=t-Date.now();return n>0?n:0}function Te(r){return r===401?"authentication_error":r===403?"permission_error":r===429?"rate_limit_error":r>=400&&r<500?"invalid_request_error":"internal_error"}var C="@cross-deck/web",q="0.10.0",U="https://api.cross-deck.com/v1",Ce=15e3,M=class{constructor(e){this.config=e}async request(e,t,n={}){if(this.config.localDevMode)return Pe(t);let s=this.buildUrl(t,n.query),a={Authorization:`Bearer ${this.config.publicKey}`,"Crossdeck-Sdk-Version":`${C}@${this.config.sdkVersion}`,Accept:"application/json"};n.idempotencyKey&&(a["Idempotency-Key"]=n.idempotencyKey);let c;n.body!==void 0&&(a["Content-Type"]="application/json",c=JSON.stringify(n.body));let i=n.timeoutMs??this.config.timeoutMs??Ce,d=typeof AbortController<"u"&&i>0?new AbortController:null,f=null;d&&i>0&&(f=setTimeout(()=>d.abort(),i));let o;try{o=await fetch(s,{method:e,headers:a,body:c,keepalive:n.keepalive===!0,signal:d?.signal})}catch(l){let u=d?.signal?.aborted===!0;throw new g({type:"network_error",code:u?"request_timeout":"fetch_failed",message:u?`Request to ${t} aborted after ${i}ms`:l instanceof Error?l.message:"fetch failed"})}finally{f!==null&&clearTimeout(f)}if(!o.ok)throw await oe(o);if(o.status!==204)try{return await o.json()}catch{throw new g({type:"internal_error",code:"invalid_json_response",message:"Server returned a 2xx with an unparseable body.",requestId:o.headers.get("x-request-id")??void 0,status:o.status})}}get isLocalDevMode(){return this.config.localDevMode===!0}buildUrl(e,t){let n=this.config.baseUrl.replace(/\/+$/,""),s=e.startsWith("/")?e:`/${e}`,a=n+s;if(t){let c=new URLSearchParams;for(let[d,f]of Object.entries(t))typeof f=="string"&&f.length>0&&c.append(d,f);let i=c.toString();i&&(a+=(a.includes("?")?"&":"?")+i)}return a}},V=null;function Pe(r){return r.startsWith("/sdk/heartbeat")?{object:"heartbeat",ok:!0,projectId:"proj_local_dev",appId:"app_local_dev",platform:"web",env:"sandbox",serverTime:Date.now()}:r.startsWith("/identity/alias")?(V||(V=`cdcust_local_${typeof crypto<"u"&&"randomUUID"in crypto?crypto.randomUUID().replace(/-/g,"").slice(0,16):Math.random().toString(36).slice(2,18)}`),{object:"alias_result",crossdeckCustomerId:V,linked:[],mergePending:!1,env:"sandbox"}):r.startsWith("/entitlements")?{object:"list",data:[],crossdeckCustomerId:V??"",env:"sandbox"}:r.startsWith("/events")?{object:"list",received:0,env:"sandbox"}:{}}var P="anon_id",A="cdcust_id",N=class{constructor(e,t,n){this.primary=e;this.prefix=t;this.secondary=n??null;let s=e.getItem(t+P),a=e.getItem(t+A),c=this.secondary?.getItem(t+P)??null,i=this.secondary?.getItem(t+A)??null,d=s??c,f=a??i;this.state={anonymousId:d??this.mintAnonymousId(),crossdeckCustomerId:f},(!s||!c)&&this.writeBoth(t+P,this.state.anonymousId),f&&(!a||!i)&&this.writeBoth(t+A,f)}get anonymousId(){return this.state.anonymousId}get crossdeckCustomerId(){return this.state.crossdeckCustomerId}setCrossdeckCustomerId(e){this.state.crossdeckCustomerId=e,this.writeBoth(this.prefix+A,e)}reset(){this.deleteBoth(this.prefix+P),this.deleteBoth(this.prefix+A),this.state={anonymousId:this.mintAnonymousId(),crossdeckCustomerId:null},this.writeBoth(this.prefix+P,this.state.anonymousId)}mintAnonymousId(){let e=Date.now().toString(36),t=k(10);return`anon_${e}${t}`}writeBoth(e,t){try{this.primary.setItem(e,t)}catch{}if(this.secondary)try{this.secondary.setItem(e,t)}catch{}}deleteBoth(e){try{this.primary.removeItem(e)}catch{}if(this.secondary)try{this.secondary.removeItem(e)}catch{}}};function k(r){let e="0123456789abcdefghijklmnopqrstuvwxyz",t=[],n=globalThis.crypto;if(n?.getRandomValues){let s=new Uint8Array(r);n.getRandomValues(s);for(let a=0;a<r;a++)t.push(e[s[a]%e.length]??"0")}else for(let s=0;s<r;s++)t.push(e[Math.floor(Math.random()*e.length)]??"0");return t.join("")}var $=class{constructor(){this.active=new Set;this.all=[];this.lastUpdated=0;this.listeners=new Set;this.listenerErrorCount=0}isEntitled(e){return this.active.has(e)}list(){return this.all.slice()}get freshness(){return this.lastUpdated}get listenerErrors(){return this.listenerErrorCount}setFromList(e){this.all=e.slice(),this.active=new Set(e.filter(t=>t.isActive).map(t=>t.key)),this.lastUpdated=Date.now(),this.notify()}clear(){this.active.clear(),this.all=[],this.lastUpdated=0,this.notify()}subscribe(e){this.listeners.add(e);let t=!1;return()=>{t||(t=!0,this.listeners.delete(e))}}notify(){if(this.listeners.size===0)return;let e=this.all.slice(),t=[...this.listeners];for(let n of t)try{n(e)}catch{this.listenerErrorCount+=1}}};function Ae(r,e,t={},n=Math.random){let s=t.baseMs??1e3,a=t.maxMs??6e4,c=t.factor??2,i=Math.min(r,30),f=Math.min(a,s*Math.pow(c,i))*n();return e!==void 0&&e>f?Math.min(a,e):Math.max(0,Math.round(f))}var K=class{constructor(e={}){this.options=e;this.attempts=0}get consecutiveFailures(){return this.attempts}get isWarning(){return this.attempts>=(this.options.failuresBeforeWarn??8)}nextDelay(e,t=Math.random){let n=Ae(this.attempts,e,this.options,t);return this.attempts+=1,n}recordSuccess(){this.attempts=0}};var x=1e3,W=class{constructor(e){this.cfg=e;this.buffer=[];this.dropped=0;this.inFlight=0;this.lastFlushAt=0;this.lastError=null;this.cancelTimer=null;this.firstFlushFired=!1;this.nextRetryAt=null;if(this.retry=new K(e.retry??{}),this.persistent=e.persistentStore??null,this.persistent){let t=this.persistent.load();t.length>0&&(t.length>x?(this.dropped+=t.length-x,this.buffer=t.slice(t.length-x)):this.buffer=t,this.cfg.onBufferChange?.(this.buffer.length),this.scheduleIdleFlush())}}enqueue(e){if(this.buffer.push(e),this.buffer.length>x){let t=this.buffer.length-x;this.buffer.splice(0,t),this.dropped+=t,this.cfg.onDrop?.(t)}this.cfg.onBufferChange?.(this.buffer.length),this.persistent?.save(this.buffer),this.buffer.length>=this.cfg.batchSize?this.flush():this.scheduleIdleFlush()}async flush(e={}){if(this.buffer.length===0)return null;this.cancelTimerIfSet(),this.nextRetryAt=null;let t=this.buffer.splice(0),n=this.mintBatchId();this.inFlight+=t.length,this.persistent?.save(this.buffer),this.cfg.onBufferChange?.(this.buffer.length);try{let s=this.cfg.envelope(),a=await this.cfg.http.request("POST","/events",{body:{appId:s.appId,environment:s.environment,sdk:s.sdk,events:t},keepalive:e.keepalive===!0,idempotencyKey:n});return this.lastFlushAt=Date.now(),this.lastError=null,this.inFlight-=t.length,this.retry.recordSuccess(),this.persistent?.save(this.buffer),this.firstFlushFired||(this.firstFlushFired=!0,this.cfg.onFirstFlushSuccess?.()),a}catch(s){this.buffer.unshift(...t),this.inFlight-=t.length;let a=s instanceof Error?s.message:String(s);this.lastError=a,this.persistent?.save(this.buffer),this.cfg.onBufferChange?.(this.buffer.length);let c=xe(s),i=this.retry.nextDelay(c);return this.scheduleRetry(i),this.cfg.onRetryScheduled?.({delayMs:i,consecutiveFailures:this.retry.consecutiveFailures,retryAfterMs:c,lastError:a}),null}}reset(){this.cancelTimerIfSet(),this.nextRetryAt=null,this.buffer=[],this.dropped=0,this.inFlight=0,this.lastError=null,this.retry.recordSuccess(),this.persistent?.clear(),this.cfg.onBufferChange?.(0)}getStats(){return{buffered:this.buffer.length,dropped:this.dropped,inFlight:this.inFlight,lastFlushAt:this.lastFlushAt,lastError:this.lastError,consecutiveFailures:this.retry.consecutiveFailures,nextRetryAt:this.nextRetryAt}}scheduleIdleFlush(){this.cancelTimerIfSet();let e=this.cfg.scheduler??ae;this.cancelTimer=e(()=>{this.flush()},this.cfg.intervalMs)}scheduleRetry(e){this.cancelTimerIfSet(),this.nextRetryAt=Date.now()+e;let t=this.cfg.scheduler??ae;this.cancelTimer=t(()=>{this.flush()},e)}cancelTimerIfSet(){this.cancelTimer&&(this.cancelTimer(),this.cancelTimer=null)}mintBatchId(){return`batch_${Date.now().toString(36)}${k(10)}`}};function xe(r){if(r&&typeof r=="object"&&"retryAfterMs"in r){let e=r.retryAfterMs;return typeof e=="number"&&Number.isFinite(e)&&e>=0?e:void 0}}function ae(r,e){let t=setTimeout(r,e);if(typeof t.unref=="function")try{t.unref()}catch{}return()=>clearTimeout(t)}var H=class{constructor(e){this.options=e;this.writeScheduled=!1;this.pendingSnapshot=null;this.key=`${e.prefix}queue.v1`}load(){let e;try{e=this.options.storage.getItem(this.key)}catch{return[]}if(!e)return[];try{let t=JSON.parse(e);return!t||t.version!==1||!Array.isArray(t.events)?[]:t.events}catch{return[]}}save(e){this.pendingSnapshot=e.slice(),!this.writeScheduled&&(this.writeScheduled=!0,queueMicrotask(()=>this.flushWrite()))}saveSync(e){this.pendingSnapshot=e.slice(),this.flushWrite()}clear(){this.pendingSnapshot=null,this.writeScheduled=!1;try{this.options.storage.removeItem(this.key)}catch{}}flushWrite(){this.writeScheduled=!1;let e=this.pendingSnapshot;if(this.pendingSnapshot=null,e===null)return;if(e.length===0){try{this.options.storage.removeItem(this.key)}catch{}return}let t={version:1,events:e};try{this.options.storage.setItem(this.key,JSON.stringify(t))}catch{}}};var w=class{constructor(){this.store=new Map}getItem(e){return this.store.get(e)??null}setItem(e,t){this.store.set(e,t)}removeItem(e){this.store.delete(e)}},j=class{constructor(e){this.maxAgeSec=e?.maxAgeSec??63072e3,this.secure=e?.secure??Re(),this.sameSite=e?.sameSite??"Lax"}getItem(e){if(!Y())return null;let t=globalThis.document,n=t.cookie?t.cookie.split(/;\s*/):[],s=encodeURIComponent(e)+"=";for(let a of n)if(a.startsWith(s))try{return decodeURIComponent(a.slice(s.length))}catch{return null}return null}setItem(e,t){if(!Y())return;let n=globalThis.document,s=[`${encodeURIComponent(e)}=${encodeURIComponent(t)}`,"Path=/",`Max-Age=${this.maxAgeSec}`,`SameSite=${this.sameSite}`];this.secure&&s.push("Secure");try{n.cookie=s.join("; ")}catch{}}removeItem(e){if(!Y())return;let t=globalThis.document,n=[`${encodeURIComponent(e)}=`,"Path=/","Max-Age=0",`SameSite=${this.sameSite}`];this.secure&&n.push("Secure");try{t.cookie=n.join("; ")}catch{}}};function ce(){try{let r=globalThis.localStorage;if(r){let e="__crossdeck_probe__";return r.setItem(e,"1"),r.removeItem(e),r}}catch{}return new w}function Re(){try{return globalThis.location?.protocol==="https:"}catch{return!1}}function Y(){return typeof globalThis.document<"u"}function De(){return typeof globalThis.window<"u"&&typeof globalThis.document<"u"&&typeof globalThis.navigator<"u"}function de(r){let e={};if(r?.appVersion&&(e.appVersion=r.appVersion),!De())return e;let t=globalThis.window,n=globalThis.navigator,s=globalThis.document;try{typeof n.language=="string"&&(e.locale=n.language)}catch{}try{e.timezone=Intl.DateTimeFormat().resolvedOptions().timeZone}catch{}try{t.screen&&(e.screenWidth=t.screen.width,e.screenHeight=t.screen.height),e.viewportWidth=t.innerWidth,e.viewportHeight=t.innerHeight,e.devicePixelRatio=t.devicePixelRatio}catch{}try{let a=n.userAgent??"",c=Fe(a);Object.assign(e,c)}catch{}try{let a=n.userAgentData;if(a?.platform&&!e.os&&(e.os=a.platform),a?.brands&&!e.browser){let c=a.brands.find(i=>!/Not[ .;A]*Brand/i.test(i.brand)&&!/Chromium/i.test(i.brand));c&&(e.browser=c.brand,e.browserVersion=c.version)}}catch{}return e}function Fe(r){let e={};if(/iPad|iPhone|iPod/.test(r)){e.os="iOS";let t=r.match(/OS (\d+[._]\d+(?:[._]\d+)?)/);t?.[1]&&(e.osVersion=t[1].replace(/_/g,"."))}else if(/Android/.test(r)){e.os="Android";let t=r.match(/Android (\d+(?:\.\d+)*)/);t?.[1]&&(e.osVersion=t[1])}else if(/Windows/.test(r)){e.os="Windows";let t=r.match(/Windows NT (\d+\.\d+)/);t?.[1]&&(e.osVersion=t[1])}else if(/Mac OS X|Macintosh/.test(r)){e.os="macOS";let t=r.match(/Mac OS X (\d+[._]\d+(?:[._]\d+)?)/);t?.[1]&&(e.osVersion=t[1].replace(/_/g,"."))}else/Linux/.test(r)&&(e.os="Linux");return/Edg\/(\d+(?:\.\d+)*)/.test(r)?(e.browser="Edge",e.browserVersion=r.match(/Edg\/(\d+(?:\.\d+)*)/)?.[1]):/Firefox\/(\d+(?:\.\d+)*)/.test(r)?(e.browser="Firefox",e.browserVersion=r.match(/Firefox\/(\d+(?:\.\d+)*)/)?.[1]):/OPR\/(\d+(?:\.\d+)*)/.test(r)?(e.browser="Opera",e.browserVersion=r.match(/OPR\/(\d+(?:\.\d+)*)/)?.[1]):/Chrome\/(\d+(?:\.\d+)*)/.test(r)?(e.browser="Chrome",e.browserVersion=r.match(/Chrome\/(\d+(?:\.\d+)*)/)?.[1]):/Version\/(\d+(?:\.\d+)*).*Safari/.test(r)&&(e.browser="Safari",e.browserVersion=r.match(/Version\/(\d+(?:\.\d+)*)/)?.[1]),e}var _={sessions:!0,pageViews:!0,deviceInfo:!0,clicks:!0,webVitals:!0},Oe=1800*1e3,Z={utm_source:"",utm_medium:"",utm_campaign:"",utm_content:"",utm_term:"",referrer:"",gclid:"",fbclid:"",msclkid:"",ttclid:"",li_fat_id:"",twclid:""},R=class{constructor(e,t){this.cfg=e;this.track=t;this.session=null;this.cleanups=[];this.pageviewId=null}install(){ue()&&(this.cfg.sessions&&this.installSessionTracking(),this.cfg.pageViews&&this.installPageViewTracking(),this.cfg.clicks&&this.installClickTracking())}uninstall(){for(;this.cleanups.length;){let e=this.cleanups.pop();try{e?.()}catch{}}this.session&&!this.session.endedSent&&this.emitSessionEnd(),this.session=null}resetSession(){this.session&&!this.session.endedSent&&this.emitSessionEnd(),this.session=this.startNewSession(),this.emitSessionStart()}get currentSessionId(){return this.session?.sessionId??null}get currentPageviewId(){return this.pageviewId}get currentAcquisition(){return this.session?.acquisition??Z}installSessionTracking(){this.session=this.startNewSession(),this.emitSessionStart();let e=()=>{if(!this.session)return;let a=globalThis.document;a.visibilityState==="hidden"?this.session.hiddenAt=Date.now():a.visibilityState==="visible"&&((this.session.hiddenAt?Date.now()-this.session.hiddenAt:0)>=Oe?(this.emitSessionEnd(),this.session=this.startNewSession(),this.emitSessionStart()):this.session.hiddenAt=null)},t=()=>this.emitSessionEnd(),n=globalThis.window,s=globalThis.document;s.addEventListener("visibilitychange",e),n.addEventListener("pagehide",t),n.addEventListener("beforeunload",t),this.cleanups.push(()=>{s.removeEventListener("visibilitychange",e),n.removeEventListener("pagehide",t),n.removeEventListener("beforeunload",t)})}startNewSession(){return{sessionId:We(),startedAt:Date.now(),hiddenAt:null,endedSent:!1,acquisition:He()}}emitSessionStart(){this.session&&this.track("session.started",{sessionId:this.session.sessionId})}emitSessionEnd(){if(!this.session||this.session.endedSent)return;let e=Date.now()-this.session.startedAt;this.track("session.ended",{sessionId:this.session.sessionId,durationMs:e}),this.session.endedSent=!0}installPageViewTracking(){let e=globalThis.window,t=globalThis.document,n=0,s="",a=250,c=(u=!1)=>{let m=e.location,h=m.href,p=Date.now();!u&&h===s&&p-n<a||(n=p,s=h,this.pageviewId=`pv_${Date.now().toString(36)}${k(10)}`,this.track("page.viewed",{pageviewId:this.pageviewId,path:m.pathname,url:h,search:m.search||void 0,hash:m.hash||void 0,title:t.title,referrer:t.referrer||void 0}))};c();let i=e.history.pushState,d=e.history.replaceState;function f(u,m,h){i.apply(this,[u,m,h]),queueMicrotask(c)}function o(u,m,h){d.apply(this,[u,m,h]),queueMicrotask(c)}e.history.pushState=f,e.history.replaceState=o;let l=()=>c(!0);e.addEventListener("popstate",l),this.cleanups.push(()=>{e.history.pushState===f&&(e.history.pushState=i),e.history.replaceState===o&&(e.history.replaceState=d),e.removeEventListener("popstate",l)})}installClickTracking(){let e=globalThis.window,t=globalThis.document,n=0,s=null,a=100,c=64,i=d=>{let f=d.target;if(!f||!(f instanceof Element))return;let o=Date.now();if(f===s&&o-n<a)return;n=o,s=f;let u=Le(f)||f;if(Ve(u)||Me(u)||qe(u))return;let m=u.tagName.toLowerCase(),h=Ne(Ue(u),c),p=u.href||void 0,v=u.target||void 0,y=u.id||void 0,S=u.getAttribute("role")||void 0,O=u.getAttribute("aria-label")||void 0,b=$e(u),I=Ke(u),T=m==="a"&&!!p,ye=u.getAttribute("data-cd-event"),E={selector:b,tag:m,text:h,elementId:y,role:S,ariaLabel:O,href:p,isLink:T,linkTarget:v,viewportX:d.clientX,viewportY:d.clientY,pageX:d.pageX,pageY:d.pageY,...I};for(let L of Object.keys(E))(E[L]===void 0||E[L]===null||E[L]==="")&&delete E[L];this.track(ye||"element.clicked",E)};t.addEventListener("click",i,{capture:!0,passive:!0}),this.cleanups.push(()=>{t.removeEventListener("click",i,{capture:!0})})}};function Le(r){return r.closest("[data-cd-event]")||r.closest("[data-cd-noTrack]")||r.closest("button, a, [role='button'], [role='link'], input[type='button'], input[type='submit']")||null}function Ve(r){if(!(r instanceof HTMLElement))return!1;let e=r.tagName.toLowerCase();if(e==="textarea"||e==="select")return!0;if(e==="input"){let t=(r.type||"").toLowerCase();return t!=="button"&&t!=="submit"&&t!=="image"&&t!=="reset"}return!1}function Me(r){return!!r.closest("[data-cd-noTrack], [data-cd-no-track], .cd-noTrack, .cd-no-track")}function qe(r){return!!r.closest('input[type="password"]')}function Ue(r){let e=r.getAttribute("aria-label");return e?e.replace(/\s+/g," ").trim():r instanceof HTMLInputElement&&r.value?r.value:(r.textContent||"").replace(/\s+/g," ").trim()}function Ne(r,e){return r.length<=e?r:r.slice(0,e-1)+"\u2026"}function $e(r){let e=[],t=r,n=0;for(;t&&t.nodeName.toLowerCase()!=="body"&&n<5;){let s=t.nodeName.toLowerCase();if(t.id){e.unshift(`${s}#${t.id}`);break}if(t.classList.length>0){let a=Array.from(t.classList).filter(c=>!c.startsWith("cd-")).slice(0,2).join(".");a&&(s+=`.${a}`)}e.unshift(s),t=t.parentElement,n++}return e.join(" > ")}function Ke(r){let e={};if(!(r instanceof HTMLElement))return e;for(let t of r.getAttributeNames()){if(!t.startsWith("data-")||t==="data-cd-noTrack"||t==="data-cd-no-track"||t==="data-cd-event")continue;let n=r.getAttribute(t)||"",s=t.replace(/^data-cd-prop-/,"").replace(/^data-/,"");e[s]=n}return e}function ue(){return typeof globalThis.window<"u"&&typeof globalThis.document<"u"}function We(){return`sess_${Date.now().toString(36)}${k(10)}`}function He(){if(!ue())return{...Z};let r={...Z};try{let e=globalThis.window,t=new URLSearchParams(e.location.search??"");r.utm_source=t.get("utm_source")??"",r.utm_medium=t.get("utm_medium")??"",r.utm_campaign=t.get("utm_campaign")??"",r.utm_content=t.get("utm_content")??"",r.utm_term=t.get("utm_term")??"",r.gclid=t.get("gclid")??"",r.fbclid=t.get("fbclid")??"",r.msclkid=t.get("msclkid")??"",r.ttclid=t.get("ttclid")??"",r.li_fat_id=t.get("li_fat_id")??"",r.twclid=t.get("twclid")??""}catch{}try{let e=globalThis.document;typeof e.referrer=="string"&&(r.referrer=e.referrer)}catch{}return r}var je=[/^email$/i,/^password$/i,/^token$/i,/^secret$/i,/^card$/i,/^phone$/i,/password/i,/credit_?card/i];function le(r){if(!r)return[];let e=[];for(let t of Object.keys(r))je.some(n=>n.test(t))&&e.push(t);return e}var B=class{constructor(){this.enabled=!1;this.seen=new Set}emit(e,t,n){if(!this.enabled)return;if(Be.has(e)){if(this.seen.has(e))return;this.seen.add(e)}let s=n?` ${ze(n)}`:"";console.info(`[crossdeck:${e}] ${t}${s}`)}},Be=new Set(["sdk.configured","sdk.first_event_sent","sdk.environment_mismatch"]);function ze(r){try{return JSON.stringify(r)}catch{return"[unserialisable context]"}}function D(r,e={}){let t=[];if(!r)return{properties:{},warnings:t};let n=e.maxStringLength??1024,s=e.maxBatchPropertyBytes??8192,a=e.maxDepth??5,c=new WeakSet,i=(o,l,u)=>{if(u>a)return t.push({kind:"depth_exceeded",key:l}),{keep:!0,value:"[depth-exceeded]"};if(o===null)return{keep:!0,value:null};let m=typeof o;if(m==="string"){let h=o;return h.length>n?(t.push({kind:"truncated_string",key:l}),{keep:!0,value:h.slice(0,n-1)+"\u2026"}):{keep:!0,value:h}}if(m==="number")return Number.isFinite(o)?{keep:!0,value:o}:(t.push({kind:"non_serialisable",key:l}),{keep:!0,value:null});if(m==="boolean")return{keep:!0,value:o};if(m==="bigint")return t.push({kind:"coerced_bigint",key:l}),{keep:!0,value:o.toString()};if(m==="function")return t.push({kind:"dropped_function",key:l}),{keep:!1,value:void 0};if(m==="symbol")return t.push({kind:"dropped_symbol",key:l}),{keep:!1,value:void 0};if(m==="undefined")return t.push({kind:"dropped_undefined",key:l}),{keep:!1,value:void 0};if(o instanceof Date)return t.push({kind:"coerced_date",key:l}),{keep:!0,value:Number.isFinite(o.getTime())?o.toISOString():null};if(o instanceof Error)return t.push({kind:"coerced_error",key:l}),{keep:!0,value:{name:o.name,message:o.message,stack:typeof o.stack=="string"?o.stack.slice(0,n):void 0}};if(o instanceof Map){t.push({kind:"coerced_map",key:l});let h={};for(let[p,v]of o.entries()){let y=typeof p=="string"?p:String(p),S=i(v,`${l}.${y}`,u+1);S.keep&&(h[y]=S.value)}return{keep:!0,value:h}}if(o instanceof Set){t.push({kind:"coerced_set",key:l});let h=[],p=0;for(let v of o.values()){let y=i(v,`${l}[${p}]`,u+1);y.keep&&h.push(y.value),p++}return{keep:!0,value:h}}if(Array.isArray(o)){if(c.has(o))return t.push({kind:"circular_reference",key:l}),{keep:!0,value:"[circular]"};c.add(o);let h=[];for(let p=0;p<o.length;p++){let v=i(o[p],`${l}[${p}]`,u+1);v.keep&&h.push(v.value)}return{keep:!0,value:h}}if(m==="object"){let h=o;if(c.has(h))return t.push({kind:"circular_reference",key:l}),{keep:!0,value:"[circular]"};c.add(h);let p={};for(let v of Object.keys(h)){let y=i(h[v],`${l}.${v}`,u+1);y.keep&&(p[v]=y.value)}return{keep:!0,value:p}}t.push({kind:"non_serialisable",key:l});try{return{keep:!0,value:String(o)}}catch{return{keep:!1,value:void 0}}},d={};for(let o of Object.keys(r)){let l=i(r[o],o,0);l.keep&&(d[o]=l.value)}let f=pe(d);if(f&&ee(f)>s){t.push({kind:"size_cap_exceeded",key:"*"});let o=Object.keys(d).map(u=>({k:u,size:ee(pe(d[u])??"")})).sort((u,m)=>m.size-u.size),l=ee(f);for(let{k:u}of o){if(l<=s)break;l-=o.find(m=>m.k===u).size,delete d[u]}d.__truncated=!0}return{properties:d,warnings:t}}function pe(r){try{return JSON.stringify(r)??null}catch{return null}}function ee(r){return typeof TextEncoder<"u"?new TextEncoder().encode(r).length:r.length*4}var z="super_props",te="groups",Q=class{constructor(e,t){this.storage=e;this.prefix=t;this.superProps={};this.groups={};this.superProps=fe(e,t+z)??{},this.groups=fe(e,t+te)??{}}register(e){for(let[t,n]of Object.entries(e))n===null?delete this.superProps[t]:n!==void 0&&(this.superProps[t]=n);return re(this.storage,this.prefix+z,this.superProps),{...this.superProps}}unregister(e){e in this.superProps&&(delete this.superProps[e],re(this.storage,this.prefix+z,this.superProps))}getSuperProperties(){return{...this.superProps}}setGroup(e,t,n){t===null?delete this.groups[e]:this.groups[e]=n!==void 0?{id:t,traits:n}:{id:t},re(this.storage,this.prefix+te,this.groups)}getGroups(){return JSON.parse(JSON.stringify(this.groups))}getGroupIds(){let e={};for(let[t,n]of Object.entries(this.groups))e[t]=n.id;return e}clear(){this.superProps={},this.groups={};try{this.storage.removeItem(this.prefix+z)}catch{}try{this.storage.removeItem(this.prefix+te)}catch{}}};function fe(r,e){let t;try{t=r.getItem(e)}catch{return null}if(!t)return null;try{return JSON.parse(t)}catch{return null}}function re(r,e,t){try{r.setItem(e,JSON.stringify(t))}catch{}}var G=class{constructor(e,t){this.cfg=e;this.report=t;this.observers=[];this.flushed=new Set;this.cls=0;this.clsEntries=[];this.inp=0;this.cleanups=[]}install(){if(!this.cfg.enabled||typeof PerformanceObserver>"u"||typeof globalThis>"u"||!("document"in globalThis))return;let e=globalThis.document;try{let a=new PerformanceObserver(c=>{for(let i of c.getEntries()){let d=i;d.responseStart>0&&!this.flushed.has("ttfb")&&(this.flushed.add("ttfb"),this.report("webvitals.ttfb",{valueMs:Math.round(d.responseStart-d.startTime)}))}});a.observe({type:"navigation",buffered:!0}),this.observers.push(a)}catch{}try{let a=new PerformanceObserver(c=>{for(let i of c.getEntries())i.name==="first-contentful-paint"&&!this.flushed.has("fcp")&&(this.flushed.add("fcp"),this.report("webvitals.fcp",{valueMs:Math.round(i.startTime)}))});a.observe({type:"paint",buffered:!0}),this.observers.push(a)}catch{}let t=0;try{let a=new PerformanceObserver(c=>{let i=c.getEntries(),d=i[i.length-1];d&&(t=d.startTime)});a.observe({type:"largest-contentful-paint",buffered:!0}),this.observers.push(a)}catch{}try{let a=new PerformanceObserver(c=>{for(let i of c.getEntries()){let d=i;typeof d.value=="number"&&!d.hadRecentInput&&(this.cls+=d.value,this.clsEntries.push(i))}});a.observe({type:"layout-shift",buffered:!0}),this.observers.push(a)}catch{}try{let a=new PerformanceObserver(c=>{for(let i of c.getEntries()){let d=i;d.interactionId&&d.duration>this.inp&&(this.inp=d.duration)}});try{a.observe({type:"event",buffered:!0,durationThreshold:16})}catch{a.observe({type:"first-input",buffered:!0})}this.observers.push(a)}catch{}let n=()=>{t>0&&!this.flushed.has("lcp")&&(this.flushed.add("lcp"),this.report("webvitals.lcp",{valueMs:Math.round(t)})),this.cls>0&&!this.flushed.has("cls")&&(this.flushed.add("cls"),this.report("webvitals.cls",{value:Math.round(this.cls*1e3)/1e3})),this.inp>0&&!this.flushed.has("inp")&&(this.flushed.add("inp"),this.report("webvitals.inp",{valueMs:Math.round(this.inp)}))},s=()=>{e.visibilityState==="hidden"&&n()};e.addEventListener("visibilitychange",s),globalThis.window.addEventListener("pagehide",n),this.cleanups.push(()=>{e.removeEventListener("visibilitychange",s),globalThis.window.removeEventListener("pagehide",n)})}uninstall(){for(let e of this.observers)try{e.disconnect()}catch{}this.observers=[];for(let e of this.cleanups.splice(0))try{e()}catch{}}};var Qe={analytics:!0,marketing:!0,errors:!0},J=class{constructor(e){this.state={...Qe};this.dntDenied=!1;e?.respectDnt&&this.detectDnt()&&(this.dntDenied=!0,this.state={analytics:!1,marketing:!1,errors:!1})}set(e){if(this.dntDenied)return{...this.state};for(let t of Object.keys(e)){let n=e[t];typeof n=="boolean"&&(this.state[t]=n)}return{...this.state}}get(){return{...this.state}}get analytics(){return this.state.analytics}get marketing(){return this.state.marketing}get errors(){return this.state.errors}get isDntDenied(){return this.dntDenied}detectDnt(){try{let e=globalThis.navigator;return e?[e.doNotTrack,e.msDoNotTrack,globalThis.doNotTrack].some(n=>n==="1"||n==="yes"):!1}catch{return!1}}},ne=/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,se=/\b\d(?:[ -]?\d){12,18}\b/g,Ge="[email]",Je="[card]";function he(r){if(!r)return r;let e=r;return ne.test(e)&&(e=e.replace(ne,Ge)),ne.lastIndex=0,se.test(e)&&(e=e.replace(se,Je)),se.lastIndex=0,e}function me(r){let e={};for(let t of Object.keys(r)){let n=r[t];typeof n=="string"?e[t]=he(n):Array.isArray(n)?e[t]=n.map(s=>typeof s=="string"?he(s):s):e[t]=n}return e}var F=class{constructor(){this.state=null}init(e){if(!e.publicKey||!e.publicKey.startsWith("cd_pub_"))throw new g({type:"configuration_error",code:"invalid_public_key",message:"Crossdeck.init requires a publishable key starting with cd_pub_."});if(!e.appId)throw new g({type:"configuration_error",code:"missing_app_id",message:"Crossdeck.init requires an appId. Find yours in the Crossdeck dashboard."});if(e.environment!=="production"&&e.environment!=="sandbox")throw new g({type:"configuration_error",code:"invalid_environment",message:'Crossdeck.init requires environment: "production" | "sandbox".'});let t=Xe(e.publicKey);if(t&&t!==e.environment)throw new g({type:"configuration_error",code:"environment_mismatch",message:`Crossdeck.init: environment "${e.environment}" disagrees with key prefix (${t}). Reconcile the publishable key with the environment declaration.`});let n=Ye(),s=e.storage??ce(),a=e.persistIdentity??!0,c=Ze(e.autoTrack),i={appId:e.appId,publicKey:e.publicKey,environment:e.environment,baseUrl:e.baseUrl??U,persistIdentity:a,storagePrefix:e.storagePrefix??"crossdeck:",autoHeartbeat:e.autoHeartbeat??!0,eventFlushBatchSize:e.eventFlushBatchSize??20,eventFlushIntervalMs:e.eventFlushIntervalMs??1500,sdkVersion:e.sdkVersion??q,autoTrack:c,appVersion:e.appVersion??null},d=new B;d.enabled=e.debug===!0;let f=new M({publicKey:i.publicKey,baseUrl:i.baseUrl,sdkVersion:i.sdkVersion,localDevMode:n});n&&console.log("[crossdeck] Localhost detected \u2014 running in dev mode (no network calls). Set publicKey: 'cd_pub_test_\u2026' and deploy to a real domain to test against the Crossdeck Sandbox.");let o=a?s:new w,u=a&&!e.storage&&typeof globalThis.document<"u"?new j:void 0,m=new N(o,i.storagePrefix,u),h=new $,p=a?new H({storage:o,prefix:i.storagePrefix}):null;p&&d.emit("sdk.queue_restored","Restored persisted event queue from a prior session.");let v=new W({http:f,batchSize:i.eventFlushBatchSize,intervalMs:i.eventFlushIntervalMs,envelope:()=>({appId:i.appId,environment:i.environment,sdk:{name:C,version:i.sdkVersion}}),persistentStore:p??void 0,onFirstFlushSuccess:()=>{d.emit("sdk.first_event_sent","First telemetry event received. View it in Live Events.",{appId:i.appId,environment:i.environment})},onRetryScheduled:b=>{d.emit("sdk.flush_retry_scheduled",`Event flush failed (${b.lastError}). Retrying in ${b.delayMs}ms (attempt ${b.consecutiveFailures}).`,{...b})}}),y=c.deviceInfo?de({appVersion:i.appVersion??void 0}):i.appVersion?{appVersion:i.appVersion}:{},S=new Q(a?o:new w,i.storagePrefix),O=new J({respectDnt:e.respectDnt===!0});if(O.isDntDenied&&d.emit("sdk.consent_dnt_applied","Do Not Track detected \u2014 all tracking dimensions denied at init."),this.state={http:f,identity:m,entitlements:h,events:v,autoTracker:null,webVitals:null,superProps:S,consent:O,scrubPii:e.scrubPii!==!1,deviceInfo:y,options:i,debug:d,developerUserId:null,uninstallUnloadFlush:null,lastServerTime:null,lastClientTime:null},d.emit("sdk.configured",`Crossdeck connected to ${i.appId} in ${i.environment} mode.`,{appId:i.appId,environment:i.environment,sdkVersion:i.sdkVersion}),c.sessions||c.pageViews){let b=new R(c,(I,T)=>this.track(I,T));this.state.autoTracker=b,b.install()}if(c.webVitals){let b=new G({enabled:!0},(I,T)=>this.track(I,T));this.state.webVitals=b,b.install()}this.state.uninstallUnloadFlush=et(()=>{this.flush({keepalive:!0}).catch(()=>{})}),i.autoHeartbeat&&!n&&this.heartbeat().catch(()=>{})}start(e){typeof console<"u"&&console.warn("[crossdeck] Crossdeck.start() is deprecated \u2014 use Crossdeck.init() instead. The signature is the same."),this.init(e)}async identify(e,t){let n=this.requireStarted();if(!e)throw new g({type:"invalid_request_error",code:"missing_user_id",message:"identify(userId) requires a non-empty userId."});if(!n.consent.analytics)return n.debug.emit("sdk.consent_denied","identify() skipped \u2014 consent denied for analytics."),{object:"alias_result",crossdeckCustomerId:n.identity.crossdeckCustomerId??"",linked:[],mergePending:!1,env:n.options.environment};let s=t?.traits!==void 0?D(t.traits):null,a=s&&Object.keys(s.properties).length>0?s.properties:void 0;if(n.debug.enabled&&s&&s.warnings.length>0)for(let d of s.warnings)n.debug.emit("sdk.property_coerced",`identify() traits key ${JSON.stringify(d.key)} was ${d.kind.replace(/_/g," ")} during validation.`,{key:d.key,kind:d.kind});let c={userId:e,anonymousId:n.identity.anonymousId};t?.email&&(c.email=t.email),a&&(c.traits=a);let i=await n.http.request("POST","/identity/alias",{body:c});return n.identity.setCrossdeckCustomerId(i.crossdeckCustomerId),n.developerUserId=e,i}register(e){let t=this.requireStarted(),n=D(e);return t.superProps.register(n.properties)}unregister(e){this.requireStarted().superProps.unregister(e)}getSuperProperties(){return this.state?this.state.superProps.getSuperProperties():{}}group(e,t,n){let s=this.requireStarted();if(!e)throw new g({type:"invalid_request_error",code:"missing_group_type",message:"group(type, id) requires a non-empty type."});let a=n?D(n).properties:void 0;s.superProps.setGroup(e,t,a)}getGroups(){return this.state?this.state.superProps.getGroups():{}}consent(e){let t=this.requireStarted(),n=t.consent.set(e);return t.debug.emit("sdk.consent_changed","Consent state updated.",{...n}),n}consentStatus(){return this.state?this.state.consent.get():{analytics:!0,marketing:!0,errors:!0}}async forget(){let e=this.requireStarted(),t=this.identityQueryParams();try{await e.http.request("POST","/identity/forget",{body:{...t}})}catch(n){e.debug.emit("sdk.consent_denied",`forget() server call failed (${n instanceof Error?n.message:String(n)}). Local state wiped anyway.`)}this.reset()}async getEntitlements(){let e=this.requireStarted(),t=this.identityQueryParams(),n=await e.http.request("GET","/entitlements",{query:t});return n.crossdeckCustomerId&&e.identity.setCrossdeckCustomerId(n.crossdeckCustomerId),e.entitlements.setFromList(n.data),n.data}isEntitled(e){return this.requireStarted().entitlements.isEntitled(e)}listEntitlements(){return this.requireStarted().entitlements.list()}onEntitlementsChange(e){return this.requireStarted().entitlements.subscribe(e)}track(e,t){let n=this.requireStarted();if(!e)throw new g({type:"invalid_request_error",code:"missing_event_name",message:"track(name) requires a non-empty name."});let s=e.startsWith("webvitals.");if(!(s?n.consent.errors:n.consent.analytics)){n.debug.enabled&&n.debug.emit("sdk.consent_denied",`Dropped event "${e}" \u2014 consent denied for ${s?"errors":"analytics"}.`);return}if(n.debug.enabled&&t){let p=le(t);p.length>0&&n.debug.emit("sdk.sensitive_property_warning",`Event "${e}" has potentially sensitive property names: ${p.join(", ")}. Crossdeck is privacy-first \u2014 avoid sending PII unless intentional.`,{eventName:e,flagged:p})}n.debug.enabled&&!n.developerUserId&&!n.identity.crossdeckCustomerId&&n.debug.emit("sdk.no_identity","Using anonymous user until identify(userId) is called.");let c=D(t);if(n.debug.enabled&&c.warnings.length>0)for(let p of c.warnings)n.debug.emit("sdk.property_coerced",`Event "${e}" property ${JSON.stringify(p.key)} was ${p.kind.replace(/_/g," ")} during validation.`,{eventName:e,key:p.key,kind:p.kind});let i={...n.deviceInfo},d=n.autoTracker?.currentSessionId;d&&(i.sessionId=d);let f=n.autoTracker?.currentPageviewId;f&&(i.pageviewId=f);let o=n.autoTracker?.currentAcquisition;o&&(o.utm_source&&(i.utm_source=o.utm_source),o.utm_medium&&(i.utm_medium=o.utm_medium),o.utm_campaign&&(i.utm_campaign=o.utm_campaign),o.utm_content&&(i.utm_content=o.utm_content),o.utm_term&&(i.utm_term=o.utm_term),o.referrer&&n.consent.marketing&&(i.referrer=o.referrer),n.consent.marketing&&(o.gclid&&(i.gclid=o.gclid),o.fbclid&&(i.fbclid=o.fbclid),o.msclkid&&(i.msclkid=o.msclkid),o.ttclid&&(i.ttclid=o.ttclid),o.li_fat_id&&(i.li_fat_id=o.li_fat_id),o.twclid&&(i.twclid=o.twclid)));let l=n.superProps.getSuperProperties();for(let p of Object.keys(l))p in i||(i[p]=l[p]);let u=n.superProps.getGroupIds();Object.keys(u).length>0&&(i.$groups=u),Object.assign(i,c.properties);let m=n.scrubPii?me(i):i,h={eventId:this.mintEventId(),name:e,timestamp:Date.now(),properties:m};Object.assign(h,this.identityHintForEvent()),n.events.enqueue(h)}async flush(e={}){await this.requireStarted().events.flush(e)}async flushEvents(){return this.flush()}async syncPurchases(e){let t=this.requireStarted();if(!e.signedTransactionInfo)throw new g({type:"invalid_request_error",code:"missing_signed_transaction_info",message:"syncPurchases requires a signedTransactionInfo string from StoreKit 2."});let n=await t.http.request("POST","/purchases/sync",{body:{rail:e.rail??"apple",...e}});return t.identity.setCrossdeckCustomerId(n.crossdeckCustomerId),t.entitlements.setFromList(n.entitlements),t.debug.emit("sdk.purchase_evidence_sent","StoreKit transaction forwarded. Waiting for backend verification.",{rail:e.rail??"apple"}),n}async purchaseApple(e){return this.syncPurchases({rail:"apple",...e})}setDebugMode(e){let t=this.requireStarted();t.debug.enabled=e,e&&t.debug.emit("sdk.configured",`Debug mode enabled for ${t.options.appId} in ${t.options.environment} mode.`,{appId:t.options.appId,environment:t.options.environment})}async heartbeat(){let e=this.requireStarted(),t=await e.http.request("GET","/sdk/heartbeat");return typeof t?.serverTime=="number"&&Number.isFinite(t.serverTime)&&(e.lastServerTime=t.serverTime,e.lastClientTime=Date.now()),t}reset(){if(this.state){if(this.state.developerUserId)try{this.track("user.signed_out",{auto:!0})}catch{}if(this.state.autoTracker?.uninstall(),this.state.identity.reset(),this.state.entitlements.clear(),this.state.events.reset(),this.state.superProps.clear(),this.state.developerUserId=null,this.state.autoTracker){let e=new R(this.state.options.autoTrack,(t,n)=>this.track(t,n));this.state.autoTracker=e,e.install()}}}diagnostics(){if(!this.state)return{started:!1,anonymousId:null,crossdeckCustomerId:null,developerUserId:null,sdkVersion:null,baseUrl:null,clock:{lastServerTime:null,lastClientTime:null,skewMs:null},entitlements:{count:0,lastUpdated:0,listenerErrors:0},events:{buffered:0,dropped:0,inFlight:0,lastFlushAt:0,lastError:null,consecutiveFailures:0,nextRetryAt:null}};let e=this.state,t=e.lastServerTime!==null&&e.lastClientTime!==null?e.lastClientTime-e.lastServerTime:null;return{started:!0,anonymousId:e.identity.anonymousId,crossdeckCustomerId:e.identity.crossdeckCustomerId,developerUserId:e.developerUserId,sdkVersion:e.options.sdkVersion,baseUrl:e.options.baseUrl,clock:{lastServerTime:e.lastServerTime,lastClientTime:e.lastClientTime,skewMs:t},entitlements:{count:e.entitlements.list().length,lastUpdated:e.entitlements.freshness,listenerErrors:e.entitlements.listenerErrors},events:e.events.getStats()}}requireStarted(){if(!this.state)throw new g({type:"configuration_error",code:"not_initialized",message:"Call Crossdeck.init({ appId, publicKey, environment }) before any other method."});return this.state}identityQueryParams(){let e=this.requireStarted();return e.identity.crossdeckCustomerId?{customerId:e.identity.crossdeckCustomerId}:e.developerUserId?{userId:e.developerUserId}:{anonymousId:e.identity.anonymousId}}identityHintForEvent(){let e=this.requireStarted(),t={anonymousId:e.identity.anonymousId};return e.developerUserId&&(t.developerUserId=e.developerUserId),e.identity.crossdeckCustomerId&&(t.crossdeckCustomerId=e.identity.crossdeckCustomerId),t}mintEventId(){return`evt_${Date.now().toString(36)}${k(8)}`}},ge=new F;function Xe(r){return r.startsWith("cd_pub_test_")?"sandbox":r.startsWith("cd_pub_live_")?"production":null}function Ye(){let r=globalThis.window;if(r?.__CROSSDECK_FORCE_LIVE__===!0)return!1;let e=r?.location?.hostname;return e?!!(e==="localhost"||e==="127.0.0.1"||e==="::1"||e==="[::1]"||e.endsWith(".local")||/^10\./.test(e)||/^192\.168\./.test(e)||/^172\.(1[6-9]|2\d|3[0-1])\./.test(e)):!1}function Ze(r){return r===!1?{sessions:!1,pageViews:!1,deviceInfo:!1,clicks:!1,webVitals:!1}:r===void 0||r===!0?{..._}:{sessions:r.sessions??_.sessions,pageViews:r.pageViews??_.pageViews,deviceInfo:r.deviceInfo??_.deviceInfo,clicks:r.clicks??_.clicks,webVitals:r.webVitals??_.webVitals}}function et(r){let e=globalThis.window,t=globalThis.document;if(!e||!t)return()=>{};let n=()=>{t.visibilityState==="hidden"&&r()},s=()=>r();return t.addEventListener("visibilitychange",n),e.addEventListener("pagehide",s),e.addEventListener("beforeunload",s),()=>{t.removeEventListener("visibilitychange",n),e.removeEventListener("pagehide",s),e.removeEventListener("beforeunload",s)}}var ie=Object.freeze([{code:"invalid_public_key",type:"configuration_error",description:"The publishable key passed to Crossdeck.init() doesn't start with cd_pub_.",resolution:"Copy the key from your Crossdeck dashboard \u2192 API keys page.",retryable:!1},{code:"missing_app_id",type:"configuration_error",description:"Crossdeck.init() was called without an appId.",resolution:"Add appId to your init options \u2014 find it in the dashboard's Apps page.",retryable:!1},{code:"invalid_environment",type:"configuration_error",description:"Crossdeck.init() requires environment: 'production' | 'sandbox'.",resolution:'Pass the literal string "production" or "sandbox" \u2014 no other values are accepted.',retryable:!1},{code:"environment_mismatch",type:"configuration_error",description:"The publishable key's env prefix doesn't match the declared environment option.",resolution:"Either change `environment` to match the key prefix (cd_pub_test_ \u2194 sandbox, cd_pub_live_ \u2194 production), or swap the key for one minted in the right env.",retryable:!1},{code:"not_initialized",type:"configuration_error",description:"An SDK method was called before Crossdeck.init().",resolution:"Call Crossdeck.init({ appId, publicKey, environment }) once at app startup before any other method.",retryable:!1},{code:"missing_user_id",type:"invalid_request_error",description:"identify() was called with an empty userId.",resolution:"Pass a stable, non-empty user identifier from your auth layer \u2014 never a hardcoded placeholder.",retryable:!1},{code:"missing_event_name",type:"invalid_request_error",description:"track() was called without an event name.",resolution:"Pass a non-empty string as the first argument.",retryable:!1},{code:"missing_group_type",type:"invalid_request_error",description:"group() was called without a group type.",resolution:'Pass a non-empty type (e.g. "org", "team") as the first argument.',retryable:!1},{code:"missing_signed_transaction_info",type:"invalid_request_error",description:"syncPurchases() was called without StoreKit 2 signed transaction info.",resolution:"Pass the JWS string from Transaction.currentEntitlements / Transaction.updates.",retryable:!1},{code:"fetch_failed",type:"network_error",description:"The underlying fetch() call failed (typically a network outage or DNS issue).",resolution:"Check the user's network. The SDK will retry automatically with exponential backoff.",retryable:!0},{code:"request_timeout",type:"network_error",description:"A request was aborted after the configured timeoutMs (default 15s).",resolution:"Check the user's connection. Increase timeoutMs in init options if the user is on a known-slow network.",retryable:!0},{code:"invalid_json_response",type:"internal_error",description:"The server returned a 2xx with an unparseable body.",resolution:"Likely a transient backend bug. Retry; if it persists, contact support with the requestId.",retryable:!0}]);function ve(r){return ie.find(e=>e.code===r)}return Ee(tt);})();
|
|
2
|
+
//# sourceMappingURL=crossdeck.umd.min.js.map
|