@cross-deck/web 1.6.4 → 1.8.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 +74 -49
- package/dist/crossdeck.umd.min.js +2 -2
- package/dist/crossdeck.umd.min.js.map +1 -1
- package/dist/error-codes.json +8 -1
- package/dist/index.cjs +222 -169
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +15 -6
- package/dist/index.d.ts +15 -6
- package/dist/index.mjs +222 -169
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +211 -158
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +211 -158
- package/dist/react.mjs.map +1 -1
- package/dist/vue.cjs +211 -158
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.mjs +211 -158
- package/dist/vue.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/contracts.json +0 -546
package/dist/index.cjs
CHANGED
|
@@ -43,6 +43,8 @@ var CrossdeckError = class _CrossdeckError extends Error {
|
|
|
43
43
|
this.requestId = payload.requestId;
|
|
44
44
|
this.status = payload.status;
|
|
45
45
|
this.retryAfterMs = payload.retryAfterMs;
|
|
46
|
+
this.minVersion = payload.minVersion;
|
|
47
|
+
this.surface = payload.surface;
|
|
46
48
|
Object.setPrototypeOf(this, _CrossdeckError.prototype);
|
|
47
49
|
}
|
|
48
50
|
};
|
|
@@ -63,7 +65,10 @@ async function crossdeckErrorFromResponse(res) {
|
|
|
63
65
|
message: envelope.message ?? `HTTP ${res.status}`,
|
|
64
66
|
requestId: envelope.request_id ?? requestId,
|
|
65
67
|
status: res.status,
|
|
66
|
-
retryAfterMs
|
|
68
|
+
retryAfterMs,
|
|
69
|
+
// PARK metadata, present only on a 426 / sdk_version_unsupported body.
|
|
70
|
+
minVersion: typeof envelope.minVersion === "string" ? envelope.minVersion : void 0,
|
|
71
|
+
surface: typeof envelope.surface === "string" ? envelope.surface : void 0
|
|
67
72
|
});
|
|
68
73
|
}
|
|
69
74
|
return new CrossdeckError({
|
|
@@ -99,7 +104,7 @@ function typeMapForStatus(status) {
|
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
// src/_version.ts
|
|
102
|
-
var SDK_VERSION = "1.
|
|
107
|
+
var SDK_VERSION = "1.8.0";
|
|
103
108
|
var SDK_NAME = "@cross-deck/web";
|
|
104
109
|
|
|
105
110
|
// src/http.ts
|
|
@@ -939,6 +944,18 @@ var EventQueue = class {
|
|
|
939
944
|
this.cancelTimer = null;
|
|
940
945
|
this.firstFlushFired = false;
|
|
941
946
|
this.nextRetryAt = null;
|
|
947
|
+
/**
|
|
948
|
+
* PARK state (HTTP 426 / `sdk_version_unsupported`). Once parked, the
|
|
949
|
+
* queue stops flushing — the server has told us our wire dialect is too
|
|
950
|
+
* old, and retrying the same payload only burns the user's battery and
|
|
951
|
+
* bandwidth and drips pointless rejects into the server logs until the
|
|
952
|
+
* app ships an upgraded SDK. The held events stay durable (persisted)
|
|
953
|
+
* and are delivered by the next boot's rehydrate, post-upgrade. Parked
|
|
954
|
+
* is per-instance: a fresh boot (the upgraded build) starts unparked.
|
|
955
|
+
*/
|
|
956
|
+
this.parked = false;
|
|
957
|
+
/** One developer-facing console warning per instance — never per-event spam. */
|
|
958
|
+
this.parkWarned = false;
|
|
942
959
|
this.retry = new RetryPolicy(cfg.retry ?? {});
|
|
943
960
|
this.persistent = cfg.persistentStore ?? null;
|
|
944
961
|
if (this.persistent) {
|
|
@@ -994,6 +1011,7 @@ var EventQueue = class {
|
|
|
994
1011
|
* flushes (pagehide / visibilitychange→hidden / beforeunload).
|
|
995
1012
|
*/
|
|
996
1013
|
async flush(options = {}) {
|
|
1014
|
+
if (this.parked) return null;
|
|
997
1015
|
let batch;
|
|
998
1016
|
let batchId;
|
|
999
1017
|
if (this.pendingBatch !== null && this.pendingBatchId !== null) {
|
|
@@ -1015,9 +1033,13 @@ var EventQueue = class {
|
|
|
1015
1033
|
const env = this.cfg.envelope();
|
|
1016
1034
|
const result = await this.cfg.http.request("POST", "/events", {
|
|
1017
1035
|
body: {
|
|
1018
|
-
//
|
|
1019
|
-
//
|
|
1020
|
-
//
|
|
1036
|
+
// Event Envelope v1 batch envelope (backend/docs/
|
|
1037
|
+
// event-envelope-spec-v1.md §1). `envelopeVersion` is the
|
|
1038
|
+
// schema/wire version the server parses against ("can I parse
|
|
1039
|
+
// this?") and is DISTINCT from `sdk.version` ("which build is in
|
|
1040
|
+
// the wild?") — two questions, two fields, never conflated. The
|
|
1041
|
+
// backend refuses payloads with a missing/unknown envelopeVersion.
|
|
1042
|
+
envelopeVersion: 1,
|
|
1021
1043
|
appId: env.appId,
|
|
1022
1044
|
environment: env.environment,
|
|
1023
1045
|
sdk: env.sdk,
|
|
@@ -1041,6 +1063,29 @@ var EventQueue = class {
|
|
|
1041
1063
|
} catch (err) {
|
|
1042
1064
|
const message = err instanceof Error ? err.message : String(err);
|
|
1043
1065
|
this.lastError = message;
|
|
1066
|
+
if (isVersionRejected(err)) {
|
|
1067
|
+
this.parked = true;
|
|
1068
|
+
this.buffer = [...batch, ...this.buffer];
|
|
1069
|
+
if (this.buffer.length > HARD_BUFFER_CAP) {
|
|
1070
|
+
const overflow = this.buffer.length - HARD_BUFFER_CAP;
|
|
1071
|
+
this.buffer.splice(0, overflow);
|
|
1072
|
+
this.dropped += overflow;
|
|
1073
|
+
}
|
|
1074
|
+
this.pendingBatch = null;
|
|
1075
|
+
this.pendingBatchId = null;
|
|
1076
|
+
this.inFlight -= batch.length;
|
|
1077
|
+
this.persistAll();
|
|
1078
|
+
this.cfg.onBufferChange?.(this.buffer.length);
|
|
1079
|
+
const minVersion = versionRejectionFloor(err);
|
|
1080
|
+
if (!this.parkWarned) {
|
|
1081
|
+
this.parkWarned = true;
|
|
1082
|
+
console.warn(
|
|
1083
|
+
`[Crossdeck] SDK outdated \u2014 the server is no longer accepting this version's event format. Your events are PARKED on-device (held, not lost) and will deliver automatically once you update @cross-deck/web${minVersion ? ` to >= ${minVersion}` : ""} and redeploy.`
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
this.cfg.onParked?.({ minVersion, surface: versionRejectionSurface(err) });
|
|
1087
|
+
return null;
|
|
1088
|
+
}
|
|
1044
1089
|
if (isPermanent4xx(err)) {
|
|
1045
1090
|
const droppedCount = batch.length;
|
|
1046
1091
|
this.pendingBatch = null;
|
|
@@ -1159,8 +1204,22 @@ function isPermanent4xx(err) {
|
|
|
1159
1204
|
if (typeof status !== "number" || !Number.isFinite(status)) return false;
|
|
1160
1205
|
if (status < 400 || status >= 500) return false;
|
|
1161
1206
|
if (status === 408 || status === 429) return false;
|
|
1207
|
+
if (status === 426) return false;
|
|
1162
1208
|
return true;
|
|
1163
1209
|
}
|
|
1210
|
+
function isVersionRejected(err) {
|
|
1211
|
+
if (!err || typeof err !== "object") return false;
|
|
1212
|
+
if (err.status === 426) return true;
|
|
1213
|
+
return err.code === "sdk_version_unsupported";
|
|
1214
|
+
}
|
|
1215
|
+
function versionRejectionFloor(err) {
|
|
1216
|
+
const v = err?.minVersion;
|
|
1217
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1218
|
+
}
|
|
1219
|
+
function versionRejectionSurface(err) {
|
|
1220
|
+
const v = err?.surface;
|
|
1221
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1222
|
+
}
|
|
1164
1223
|
function defaultScheduler(fn, ms) {
|
|
1165
1224
|
const id = setTimeout(fn, ms);
|
|
1166
1225
|
if (typeof id.unref === "function") {
|
|
@@ -1472,6 +1531,15 @@ var AutoTracker = class {
|
|
|
1472
1531
|
this.cleanups = [];
|
|
1473
1532
|
/** Last time we flushed lastActivityAt to storage (throttle gate). */
|
|
1474
1533
|
this.lastPersistAt = 0;
|
|
1534
|
+
/**
|
|
1535
|
+
* Event Envelope v1 §3 — per-session monotonic sequence counter.
|
|
1536
|
+
* The single owner of the seq counter lives here, alongside all other
|
|
1537
|
+
* session state. Incremented atomically at track() time (via nextSeq()),
|
|
1538
|
+
* reset to 0 whenever a new session boundary is crossed (startNewSession
|
|
1539
|
+
* path + resetSession). Persists across background/foreground within a
|
|
1540
|
+
* session — only a real session boundary (not a tab hide/show) resets it.
|
|
1541
|
+
*/
|
|
1542
|
+
this._sessionSeq = 0;
|
|
1475
1543
|
/**
|
|
1476
1544
|
* Stable per-page-view identifier. Minted at every `page.viewed`
|
|
1477
1545
|
* emission and attached to every subsequent event until the next
|
|
@@ -1556,6 +1624,20 @@ var AutoTracker = class {
|
|
|
1556
1624
|
get currentAcquisition() {
|
|
1557
1625
|
return this.session?.acquisition ?? EMPTY_ACQUISITION;
|
|
1558
1626
|
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Event Envelope v1 §3 — return the next seq value for the current session
|
|
1629
|
+
* and advance the counter. Called SYNCHRONOUSLY at track() time, before any
|
|
1630
|
+
* async dispatch, so the seq assignment order is deterministic (call order,
|
|
1631
|
+
* not scheduler luck). The counter is reset to 0 at every session boundary;
|
|
1632
|
+
* it is NOT reset by tab hide/show (spec §3 clause 1: backgrounding does not
|
|
1633
|
+
* reset seq). When no session is active (Node, before init, after uninstall),
|
|
1634
|
+
* returns 0 and leaves the counter unchanged — fallback, not an error.
|
|
1635
|
+
*/
|
|
1636
|
+
nextSeq() {
|
|
1637
|
+
const seq = this._sessionSeq;
|
|
1638
|
+
this._sessionSeq += 1;
|
|
1639
|
+
return seq;
|
|
1640
|
+
}
|
|
1559
1641
|
// ---------- sessions ----------
|
|
1560
1642
|
installSessionTracking() {
|
|
1561
1643
|
const now = Date.now();
|
|
@@ -1607,6 +1689,7 @@ var AutoTracker = class {
|
|
|
1607
1689
|
}
|
|
1608
1690
|
startNewSession() {
|
|
1609
1691
|
const now = Date.now();
|
|
1692
|
+
this._sessionSeq = 0;
|
|
1610
1693
|
return {
|
|
1611
1694
|
sessionId: mintSessionId(),
|
|
1612
1695
|
startedAt: now,
|
|
@@ -2335,6 +2418,40 @@ function writeJson(storage, key, value) {
|
|
|
2335
2418
|
}
|
|
2336
2419
|
}
|
|
2337
2420
|
|
|
2421
|
+
// src/internal-opt-out.ts
|
|
2422
|
+
var INTERNAL_OPT_OUT_PROPERTY = "$crossdeck_internal";
|
|
2423
|
+
var STORAGE_KEY = "crossdeck.internalOptOut";
|
|
2424
|
+
function localStore() {
|
|
2425
|
+
try {
|
|
2426
|
+
return typeof localStorage !== "undefined" ? localStorage : null;
|
|
2427
|
+
} catch {
|
|
2428
|
+
return null;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
function processInternalOptOutUrl() {
|
|
2432
|
+
try {
|
|
2433
|
+
const search = typeof location !== "undefined" ? location.search : "";
|
|
2434
|
+
const params = new URLSearchParams(search || "");
|
|
2435
|
+
if (!params.has("crossdeck_internal")) return;
|
|
2436
|
+
const store = localStore();
|
|
2437
|
+
if (!store) return;
|
|
2438
|
+
const v = params.get("crossdeck_internal");
|
|
2439
|
+
if (v === "1" || v === "true") {
|
|
2440
|
+
store.setItem(STORAGE_KEY, "1");
|
|
2441
|
+
} else if (v === "0" || v === "false") {
|
|
2442
|
+
store.removeItem(STORAGE_KEY);
|
|
2443
|
+
}
|
|
2444
|
+
} catch {
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
function isInternalOptOut() {
|
|
2448
|
+
try {
|
|
2449
|
+
return localStore()?.getItem(STORAGE_KEY) === "1";
|
|
2450
|
+
} catch {
|
|
2451
|
+
return false;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2338
2455
|
// src/web-vitals.ts
|
|
2339
2456
|
var WebVitalsTracker = class {
|
|
2340
2457
|
constructor(cfg, report) {
|
|
@@ -2843,6 +2960,13 @@ var CROSSDECK_ERROR_CODES = Object.freeze([
|
|
|
2843
2960
|
description: "A field is present but the value failed validation (wrong shape, wrong length, wrong enum value).",
|
|
2844
2961
|
resolution: "Read error.message for the specific field + reason. SDK-managed call sites should never emit this \u2014 file a bug if you do.",
|
|
2845
2962
|
retryable: false
|
|
2963
|
+
},
|
|
2964
|
+
{
|
|
2965
|
+
code: "sdk_version_unsupported",
|
|
2966
|
+
type: "version_error",
|
|
2967
|
+
description: "HTTP 426 \u2014 your installed SDK sends an event format the server no longer accepts. The data is good; only the wire dialect is too old. The SDK PARKS automatically: events are held on-device (paused, not lost) and deliver on the next launch after you upgrade.",
|
|
2968
|
+
resolution: "Update @cross-deck/web to at least the version in error.minVersion and redeploy \u2014 the parked queue backfills automatically. See https://cross-deck.com/docs/sdk-event-durability/.",
|
|
2969
|
+
retryable: false
|
|
2846
2970
|
}
|
|
2847
2971
|
]);
|
|
2848
2972
|
function getErrorCode(code) {
|
|
@@ -2865,15 +2989,6 @@ var VerifierReporter = class {
|
|
|
2865
2989
|
constructor(ctx) {
|
|
2866
2990
|
this.ctx = ctx;
|
|
2867
2991
|
this.reentrancyDepth = 0;
|
|
2868
|
-
/** The live verifier set — attached by the SDK right after init so the
|
|
2869
|
-
* emit path can schema-lock its OWN outgoing telemetry against the real
|
|
2870
|
-
* payload. Null until attached (boot-time reports still work without it). */
|
|
2871
|
-
this.verifiers = null;
|
|
2872
|
-
}
|
|
2873
|
-
/** Hand the reporter the live verifier set so `reportFail` can run the
|
|
2874
|
-
* `onReportContractFailure` hook on the exact payload it's about to emit. */
|
|
2875
|
-
attachVerifiers(verifiers) {
|
|
2876
|
-
this.verifiers = verifiers;
|
|
2877
2992
|
}
|
|
2878
2993
|
/**
|
|
2879
2994
|
* Report a verifier result. Pass `operation` for hot-path results
|
|
@@ -2902,7 +3017,7 @@ var VerifierReporter = class {
|
|
|
2902
3017
|
if (this.reentrancyDepth > 0) return;
|
|
2903
3018
|
this.reentrancyDepth += 1;
|
|
2904
3019
|
try {
|
|
2905
|
-
|
|
3020
|
+
this.ctx.emitTelemetry({
|
|
2906
3021
|
contract_id: result.contractId,
|
|
2907
3022
|
sdk_version: this.ctx.sdkVersion,
|
|
2908
3023
|
sdk_platform: "web",
|
|
@@ -2910,13 +3025,7 @@ var VerifierReporter = class {
|
|
|
2910
3025
|
run_context: this.ctx.runContext,
|
|
2911
3026
|
run_id: this.ctx.runId,
|
|
2912
3027
|
verification_phase: phase
|
|
2913
|
-
};
|
|
2914
|
-
if (this.verifiers) {
|
|
2915
|
-
runOnReportContractFailure(this.verifiers, this, this.ctx, {
|
|
2916
|
-
outgoingPayload: payload
|
|
2917
|
-
});
|
|
2918
|
-
}
|
|
2919
|
-
this.ctx.emitTelemetry(payload);
|
|
3028
|
+
});
|
|
2920
3029
|
} finally {
|
|
2921
3030
|
this.reentrancyDepth -= 1;
|
|
2922
3031
|
}
|
|
@@ -3206,31 +3315,9 @@ var VERIFIER_FLUSH_INTERVAL_PARITY = {
|
|
|
3206
3315
|
"eventFlushIntervalMs default = 2000ms (Web/Node/RN/Swift/Android parity)",
|
|
3207
3316
|
nowMs() - t0
|
|
3208
3317
|
);
|
|
3209
|
-
},
|
|
3210
|
-
// Hot-path hook — on every real track() we re-affirm the LIVE configured
|
|
3211
|
-
// flush interval (carried on the observation), so the parity guarantee is
|
|
3212
|
-
// verified against the running SDK in the customer flow, not just a
|
|
3213
|
-
// canonical constant at boot. It can't change per-event, but observing it
|
|
3214
|
-
// per-event is what makes the contract genuinely runtime-verified.
|
|
3215
|
-
hooks: {
|
|
3216
|
-
onTrack(obs) {
|
|
3217
|
-
const t0 = nowMs();
|
|
3218
|
-
const CANONICAL_DEFAULT_MS = 2e3;
|
|
3219
|
-
const ms = obs.flushIntervalMs;
|
|
3220
|
-
if (typeof ms !== "number" || ms < 100 || ms > 6e4) {
|
|
3221
|
-
return fail(
|
|
3222
|
-
"flush-interval-parity",
|
|
3223
|
-
`live eventFlushIntervalMs=${ms} outside reasonable bounds [100, 60000]`,
|
|
3224
|
-
nowMs() - t0
|
|
3225
|
-
);
|
|
3226
|
-
}
|
|
3227
|
-
return pass(
|
|
3228
|
-
"flush-interval-parity",
|
|
3229
|
-
ms === CANONICAL_DEFAULT_MS ? "live eventFlushIntervalMs = 2000ms (canonical parity value)" : `live eventFlushIntervalMs = ${ms}ms (permitted override; canonical default 2000ms)`,
|
|
3230
|
-
nowMs() - t0
|
|
3231
|
-
);
|
|
3232
|
-
}
|
|
3233
3318
|
}
|
|
3319
|
+
// No hot-path hook — the flush interval is set once at start() and
|
|
3320
|
+
// never changes per-operation.
|
|
3234
3321
|
};
|
|
3235
3322
|
function buildFlushIntervalVerifier(configuredIntervalMs) {
|
|
3236
3323
|
return {
|
|
@@ -3357,26 +3444,6 @@ var CONTRACT_FAILED_FORBIDDEN_FIELDS = [
|
|
|
3357
3444
|
"referrer",
|
|
3358
3445
|
"deviceId"
|
|
3359
3446
|
];
|
|
3360
|
-
function contractFailedSchemaViolation(keys) {
|
|
3361
|
-
const allowed = /* @__PURE__ */ new Set([
|
|
3362
|
-
...CONTRACT_FAILED_REQUIRED_FIELDS,
|
|
3363
|
-
...CONTRACT_FAILED_OPTIONAL_FIELDS
|
|
3364
|
-
]);
|
|
3365
|
-
const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
|
|
3366
|
-
for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
|
|
3367
|
-
if (!keys.includes(required)) return `missing required field: ${required}`;
|
|
3368
|
-
}
|
|
3369
|
-
for (const k of keys) {
|
|
3370
|
-
if (forbidden.has(k)) return `forbidden field on wire: ${k}`;
|
|
3371
|
-
}
|
|
3372
|
-
for (const k of keys) {
|
|
3373
|
-
if (!allowed.has(k)) {
|
|
3374
|
-
return `unrecognised field on wire: ${k} (not in required \u222A optional)`;
|
|
3375
|
-
}
|
|
3376
|
-
}
|
|
3377
|
-
return null;
|
|
3378
|
-
}
|
|
3379
|
-
var CONTRACT_FAILED_OK_EVIDENCE = `fields \u2286 required(${CONTRACT_FAILED_REQUIRED_FIELDS.length}) \u222A optional(${CONTRACT_FAILED_OPTIONAL_FIELDS.length}); ${CONTRACT_FAILED_FORBIDDEN_FIELDS.length} forbidden absent`;
|
|
3380
3447
|
var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
|
|
3381
3448
|
contractId: "contract-failed-payload-schema-lock",
|
|
3382
3449
|
bootTest() {
|
|
@@ -3391,35 +3458,43 @@ var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
|
|
|
3391
3458
|
verification_phase: "boot"
|
|
3392
3459
|
};
|
|
3393
3460
|
const keys = Object.keys(syntheticPayload);
|
|
3394
|
-
const
|
|
3395
|
-
|
|
3396
|
-
|
|
3461
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
3462
|
+
...CONTRACT_FAILED_REQUIRED_FIELDS,
|
|
3463
|
+
...CONTRACT_FAILED_OPTIONAL_FIELDS
|
|
3464
|
+
]);
|
|
3465
|
+
const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
|
|
3466
|
+
for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
|
|
3467
|
+
if (!keys.includes(required)) {
|
|
3468
|
+
return fail(
|
|
3469
|
+
"contract-failed-payload-schema-lock",
|
|
3470
|
+
`missing required field: ${required}`,
|
|
3471
|
+
nowMs() - t0
|
|
3472
|
+
);
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
for (const k of keys) {
|
|
3476
|
+
if (forbidden.has(k)) {
|
|
3477
|
+
return fail(
|
|
3478
|
+
"contract-failed-payload-schema-lock",
|
|
3479
|
+
`forbidden field on wire: ${k}`,
|
|
3480
|
+
nowMs() - t0
|
|
3481
|
+
);
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
for (const k of keys) {
|
|
3485
|
+
if (!allowed.has(k)) {
|
|
3486
|
+
return fail(
|
|
3487
|
+
"contract-failed-payload-schema-lock",
|
|
3488
|
+
`unrecognised field on wire: ${k} (not in required \u222A optional)`,
|
|
3489
|
+
nowMs() - t0
|
|
3490
|
+
);
|
|
3491
|
+
}
|
|
3397
3492
|
}
|
|
3398
3493
|
return pass(
|
|
3399
3494
|
"contract-failed-payload-schema-lock",
|
|
3400
|
-
`${keys.length} ${
|
|
3495
|
+
`${keys.length} fields \u2286 required(${CONTRACT_FAILED_REQUIRED_FIELDS.length}) \u222A optional(${CONTRACT_FAILED_OPTIONAL_FIELDS.length}); ${CONTRACT_FAILED_FORBIDDEN_FIELDS.length} forbidden absent`,
|
|
3401
3496
|
nowMs() - t0
|
|
3402
3497
|
);
|
|
3403
|
-
},
|
|
3404
|
-
// Hot-path hook — fires on the REAL reportFail emit (VerifierReporter,
|
|
3405
|
-
// inside the re-entrancy guard) against the exact payload about to go on
|
|
3406
|
-
// the wire. This is the assertion the guard at reportFail was built in
|
|
3407
|
-
// anticipation of: every reliability-channel write is schema-locked in the
|
|
3408
|
-
// field, against real data, not just the synthetic boot mirror.
|
|
3409
|
-
hooks: {
|
|
3410
|
-
onReportContractFailure(obs) {
|
|
3411
|
-
const t0 = nowMs();
|
|
3412
|
-
const keys = Object.keys(obs.outgoingPayload);
|
|
3413
|
-
const violation = contractFailedSchemaViolation(keys);
|
|
3414
|
-
if (violation) {
|
|
3415
|
-
return fail("contract-failed-payload-schema-lock", violation, nowMs() - t0);
|
|
3416
|
-
}
|
|
3417
|
-
return pass(
|
|
3418
|
-
"contract-failed-payload-schema-lock",
|
|
3419
|
-
`${keys.length} ${CONTRACT_FAILED_OK_EVIDENCE}`,
|
|
3420
|
-
nowMs() - t0
|
|
3421
|
-
);
|
|
3422
|
-
}
|
|
3423
3498
|
}
|
|
3424
3499
|
};
|
|
3425
3500
|
var BACKEND_WIRE_CODES = Object.freeze([
|
|
@@ -3437,7 +3512,8 @@ var BACKEND_WIRE_CODES = Object.freeze([
|
|
|
3437
3512
|
"google_not_supported",
|
|
3438
3513
|
"stripe_not_supported",
|
|
3439
3514
|
"missing_required_param",
|
|
3440
|
-
"invalid_param_value"
|
|
3515
|
+
"invalid_param_value",
|
|
3516
|
+
"sdk_version_unsupported"
|
|
3441
3517
|
]);
|
|
3442
3518
|
var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
|
|
3443
3519
|
contractId: "sdk-error-codes-catalogue",
|
|
@@ -3470,39 +3546,6 @@ var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
|
|
|
3470
3546
|
nowMs() - t0
|
|
3471
3547
|
);
|
|
3472
3548
|
}
|
|
3473
|
-
},
|
|
3474
|
-
// Hot-path hook — when the SDK parses a real wire error, assert the code
|
|
3475
|
-
// the customer actually hit carries usable remediation in the shipped
|
|
3476
|
-
// catalogue. Scoped to BACKEND_WIRE_CODES (the contract's set); other
|
|
3477
|
-
// codes are out of scope and pass. This turns the catalogue from a
|
|
3478
|
-
// boot-only completeness claim into a per-error field assertion: a backend
|
|
3479
|
-
// code that ships without remediation is caught the first time a customer
|
|
3480
|
-
// hits it, not only if the off-by-default boot self-test ran.
|
|
3481
|
-
hooks: {
|
|
3482
|
-
onErrorParse(obs) {
|
|
3483
|
-
const t0 = nowMs();
|
|
3484
|
-
const code = obs.errorCode;
|
|
3485
|
-
if (!code || !BACKEND_WIRE_CODES.includes(code)) {
|
|
3486
|
-
return pass(
|
|
3487
|
-
"sdk-error-codes-catalogue",
|
|
3488
|
-
`wire code "${code || "(none)"}" is out of the backend-catalogue scope`,
|
|
3489
|
-
nowMs() - t0
|
|
3490
|
-
);
|
|
3491
|
-
}
|
|
3492
|
-
const entry = getErrorCode(code);
|
|
3493
|
-
if (!entry || !entry.description || entry.description.trim().length === 0 || !entry.resolution || entry.resolution.trim().length === 0) {
|
|
3494
|
-
return fail(
|
|
3495
|
-
"sdk-error-codes-catalogue",
|
|
3496
|
-
`backend wire code "${code}" hit in the field has no description+resolution in the shipped catalogue`,
|
|
3497
|
-
nowMs() - t0
|
|
3498
|
-
);
|
|
3499
|
-
}
|
|
3500
|
-
return pass(
|
|
3501
|
-
"sdk-error-codes-catalogue",
|
|
3502
|
-
`backend wire code "${code}" carries description + resolution`,
|
|
3503
|
-
nowMs() - t0
|
|
3504
|
-
);
|
|
3505
|
-
}
|
|
3506
3549
|
}
|
|
3507
3550
|
};
|
|
3508
3551
|
var STATIC_VERIFIERS = Object.freeze([
|
|
@@ -3626,24 +3669,6 @@ function runOnErrorParse(verifiers, reporter, ctx, obs) {
|
|
|
3626
3669
|
reporter.report(result, "hot_path", "errorParse");
|
|
3627
3670
|
}
|
|
3628
3671
|
}
|
|
3629
|
-
function runOnReportContractFailure(verifiers, reporter, ctx, obs) {
|
|
3630
|
-
if (ctx.disableContractAssertions) return;
|
|
3631
|
-
for (const verifier of verifiers) {
|
|
3632
|
-
const hook = verifier.hooks?.onReportContractFailure;
|
|
3633
|
-
if (!hook) continue;
|
|
3634
|
-
let result;
|
|
3635
|
-
try {
|
|
3636
|
-
result = hook(obs);
|
|
3637
|
-
} catch (err) {
|
|
3638
|
-
result = fail(
|
|
3639
|
-
verifier.contractId,
|
|
3640
|
-
`hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
|
|
3641
|
-
0
|
|
3642
|
-
);
|
|
3643
|
-
}
|
|
3644
|
-
reporter.report(result, "hot_path", "reportContractFailure");
|
|
3645
|
-
}
|
|
3646
|
-
}
|
|
3647
3672
|
function defaultDebugModeFlag() {
|
|
3648
3673
|
try {
|
|
3649
3674
|
if (typeof process !== "undefined" && process.env) {
|
|
@@ -3795,18 +3820,18 @@ function isInAppFrame(filename) {
|
|
|
3795
3820
|
if (/\/crossdeck\.umd\.min\.js$/.test(filename)) return false;
|
|
3796
3821
|
return true;
|
|
3797
3822
|
}
|
|
3798
|
-
function fingerprintError(message, frames,
|
|
3823
|
+
function fingerprintError(message, frames, location2) {
|
|
3799
3824
|
const inAppFrames = frames.filter((f) => f.in_app).slice(0, 3);
|
|
3800
3825
|
const parts = [
|
|
3801
3826
|
(message || "").slice(0, 200),
|
|
3802
3827
|
...inAppFrames.map((f) => `${f.function}@${f.filename}:${f.lineno}`)
|
|
3803
3828
|
];
|
|
3804
|
-
if (inAppFrames.length === 0 &&
|
|
3829
|
+
if (inAppFrames.length === 0 && location2) {
|
|
3805
3830
|
const loc = [
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3831
|
+
location2.errorType ?? "",
|
|
3832
|
+
location2.filename ?? "",
|
|
3833
|
+
location2.lineno ?? "",
|
|
3834
|
+
location2.colno ?? ""
|
|
3810
3835
|
].join(":");
|
|
3811
3836
|
if (loc !== ":::") parts.push(loc);
|
|
3812
3837
|
}
|
|
@@ -4476,9 +4501,6 @@ var CrossdeckClient = class {
|
|
|
4476
4501
|
this.verifiers = null;
|
|
4477
4502
|
this.verifierReporter = null;
|
|
4478
4503
|
this.verifierCtx = null;
|
|
4479
|
-
// The live configured event-flush interval, surfaced to the track-path
|
|
4480
|
-
// verifier so flush-interval-parity validates the real cadence per event.
|
|
4481
|
-
this.flushIntervalMs = 2e3;
|
|
4482
4504
|
}
|
|
4483
4505
|
/**
|
|
4484
4506
|
* Boot the SDK. Idempotent — calling init twice with the same options
|
|
@@ -4648,6 +4670,13 @@ var CrossdeckClient = class {
|
|
|
4648
4670
|
headline,
|
|
4649
4671
|
{ ...info }
|
|
4650
4672
|
);
|
|
4673
|
+
},
|
|
4674
|
+
onParked: (info) => {
|
|
4675
|
+
debug.emit(
|
|
4676
|
+
"sdk.parked",
|
|
4677
|
+
`[crossdeck] SDK parked \u2014 server no longer accepts this version's event format. Events held on-device (paused, not lost); update @cross-deck/web${info.minVersion ? ` to >= ${info.minVersion}` : ""} to resume.`,
|
|
4678
|
+
{ ...info }
|
|
4679
|
+
);
|
|
4651
4680
|
}
|
|
4652
4681
|
});
|
|
4653
4682
|
const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
|
|
@@ -4655,6 +4684,7 @@ var CrossdeckClient = class {
|
|
|
4655
4684
|
persistIdentity ? effectiveStorage : new MemoryStorage(),
|
|
4656
4685
|
opts.storagePrefix
|
|
4657
4686
|
);
|
|
4687
|
+
processInternalOptOutUrl();
|
|
4658
4688
|
const consent = new ConsentManager({ respectDnt: options.respectDnt === true });
|
|
4659
4689
|
if (consent.isDntDenied) {
|
|
4660
4690
|
debug.emit(
|
|
@@ -4758,8 +4788,6 @@ var CrossdeckClient = class {
|
|
|
4758
4788
|
this.verifierReporter = new VerifierReporter(this.verifierCtx);
|
|
4759
4789
|
const flushVerifier = buildFlushIntervalVerifier(opts.eventFlushIntervalMs);
|
|
4760
4790
|
this.verifiers = Object.freeze([...STATIC_VERIFIERS, flushVerifier]);
|
|
4761
|
-
this.flushIntervalMs = opts.eventFlushIntervalMs;
|
|
4762
|
-
this.verifierReporter.attachVerifiers(this.verifiers);
|
|
4763
4791
|
if (localDevMode) {
|
|
4764
4792
|
const verifyAtBootLocal = options.verifyContractsAtBoot ?? debugDefault;
|
|
4765
4793
|
if (verifyAtBootLocal && this.verifierReporter) {
|
|
@@ -5310,8 +5338,27 @@ var CrossdeckClient = class {
|
|
|
5310
5338
|
);
|
|
5311
5339
|
}
|
|
5312
5340
|
}
|
|
5341
|
+
const seq = s.autoTracker?.nextSeq() ?? 0;
|
|
5342
|
+
const occurrenceTimestamp = Date.now();
|
|
5313
5343
|
s.autoTracker?.markActivity();
|
|
5314
|
-
const
|
|
5344
|
+
const wireContext = {};
|
|
5345
|
+
const di = s.deviceInfo;
|
|
5346
|
+
if (di.os) wireContext.os = di.os;
|
|
5347
|
+
if (di.osVersion) wireContext.osVersion = di.osVersion;
|
|
5348
|
+
const appVer = s.options.appVersion;
|
|
5349
|
+
if (appVer) wireContext.appVersion = appVer;
|
|
5350
|
+
wireContext.sdkName = SDK_NAME;
|
|
5351
|
+
wireContext.sdkVersion = s.options.sdkVersion;
|
|
5352
|
+
if (di.locale) wireContext.locale = di.locale;
|
|
5353
|
+
if (di.timezone) wireContext.timezone = di.timezone;
|
|
5354
|
+
if (di.browser) wireContext.browser = di.browser;
|
|
5355
|
+
if (di.browserVersion) wireContext.browserVersion = di.browserVersion;
|
|
5356
|
+
const enriched = {};
|
|
5357
|
+
if (di.screenWidth !== void 0) enriched.screenWidth = di.screenWidth;
|
|
5358
|
+
if (di.screenHeight !== void 0) enriched.screenHeight = di.screenHeight;
|
|
5359
|
+
if (di.viewportWidth !== void 0) enriched.viewportWidth = di.viewportWidth;
|
|
5360
|
+
if (di.viewportHeight !== void 0) enriched.viewportHeight = di.viewportHeight;
|
|
5361
|
+
if (di.devicePixelRatio !== void 0) enriched.devicePixelRatio = di.devicePixelRatio;
|
|
5315
5362
|
const sessionId = s.autoTracker?.currentSessionId;
|
|
5316
5363
|
if (sessionId) enriched.sessionId = sessionId;
|
|
5317
5364
|
const pageviewId = s.autoTracker?.currentPageviewId;
|
|
@@ -5334,17 +5381,24 @@ var CrossdeckClient = class {
|
|
|
5334
5381
|
}
|
|
5335
5382
|
}
|
|
5336
5383
|
const supers = s.superProps.getSuperProperties();
|
|
5337
|
-
Object.
|
|
5384
|
+
for (const k of Object.keys(supers)) {
|
|
5385
|
+
if (!(k in enriched)) enriched[k] = supers[k];
|
|
5386
|
+
}
|
|
5338
5387
|
const groupIds = s.superProps.getGroupIds();
|
|
5339
5388
|
if (Object.keys(groupIds).length > 0) {
|
|
5340
5389
|
enriched.$groups = groupIds;
|
|
5341
5390
|
}
|
|
5342
5391
|
Object.assign(enriched, validation.properties);
|
|
5392
|
+
if (isInternalOptOut()) {
|
|
5393
|
+
enriched[INTERNAL_OPT_OUT_PROPERTY] = true;
|
|
5394
|
+
}
|
|
5343
5395
|
const finalProperties = s.scrubPii ? scrubPiiFromProperties(enriched) : enriched;
|
|
5344
5396
|
const event = {
|
|
5345
5397
|
eventId: this.mintEventId(),
|
|
5346
5398
|
name,
|
|
5347
|
-
timestamp:
|
|
5399
|
+
timestamp: occurrenceTimestamp,
|
|
5400
|
+
seq,
|
|
5401
|
+
context: wireContext,
|
|
5348
5402
|
properties: finalProperties
|
|
5349
5403
|
};
|
|
5350
5404
|
Object.assign(event, this.identityHintForEvent());
|
|
@@ -5354,8 +5408,7 @@ var CrossdeckClient = class {
|
|
|
5354
5408
|
callerProperties: validation.properties,
|
|
5355
5409
|
superProperties: supers,
|
|
5356
5410
|
deviceProperties: s.deviceInfo,
|
|
5357
|
-
mergedProperties: enriched
|
|
5358
|
-
flushIntervalMs: this.flushIntervalMs
|
|
5411
|
+
mergedProperties: enriched
|
|
5359
5412
|
});
|
|
5360
5413
|
}
|
|
5361
5414
|
if (!isError && !isWebVital) {
|
|
@@ -5700,8 +5753,8 @@ function installUnloadFlush(onUnload) {
|
|
|
5700
5753
|
}
|
|
5701
5754
|
|
|
5702
5755
|
// src/_contracts-bundled.ts
|
|
5703
|
-
var BUNDLED_IN = "@cross-deck/web@1.
|
|
5704
|
-
var SDK_VERSION2 = "1.
|
|
5756
|
+
var BUNDLED_IN = "@cross-deck/web@1.8.0";
|
|
5757
|
+
var SDK_VERSION2 = "1.8.0";
|
|
5705
5758
|
var BUNDLED_CONTRACTS = Object.freeze([
|
|
5706
5759
|
{
|
|
5707
5760
|
"id": "contract-failed-payload-schema-lock",
|
|
@@ -5823,7 +5876,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5823
5876
|
"legal/security/index.html#diagnostic",
|
|
5824
5877
|
"legal/sdk-data/index.html#b-diagnostic"
|
|
5825
5878
|
],
|
|
5826
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5879
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
5827
5880
|
"runtimeVerified": true
|
|
5828
5881
|
},
|
|
5829
5882
|
{
|
|
@@ -5863,7 +5916,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5863
5916
|
],
|
|
5864
5917
|
"registeredAt": "2026-05-26",
|
|
5865
5918
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 8 (codifies existing contract)",
|
|
5866
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5919
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
5867
5920
|
"runtimeVerified": true
|
|
5868
5921
|
},
|
|
5869
5922
|
{
|
|
@@ -5909,7 +5962,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5909
5962
|
],
|
|
5910
5963
|
"registeredAt": "2026-05-26",
|
|
5911
5964
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.3",
|
|
5912
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5965
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
5913
5966
|
"runtimeVerified": true
|
|
5914
5967
|
},
|
|
5915
5968
|
{
|
|
@@ -6015,7 +6068,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
6015
6068
|
],
|
|
6016
6069
|
"registeredAt": "2026-05-26",
|
|
6017
6070
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 2.2.a + 2.2.b + 2.2.c",
|
|
6018
|
-
"bundledIn": "@cross-deck/web@1.
|
|
6071
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
6019
6072
|
"runtimeVerified": true
|
|
6020
6073
|
},
|
|
6021
6074
|
{
|
|
@@ -6043,7 +6096,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
6043
6096
|
],
|
|
6044
6097
|
"registeredAt": "2026-05-26",
|
|
6045
6098
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 5.5",
|
|
6046
|
-
"bundledIn": "@cross-deck/web@1.
|
|
6099
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
6047
6100
|
"runtimeVerified": false
|
|
6048
6101
|
},
|
|
6049
6102
|
{
|
|
@@ -6123,7 +6176,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
6123
6176
|
],
|
|
6124
6177
|
"registeredAt": "2026-05-26",
|
|
6125
6178
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 1.3 (web/RN) + dogfood-gap fix (swift + android)",
|
|
6126
|
-
"bundledIn": "@cross-deck/web@1.
|
|
6179
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
6127
6180
|
"runtimeVerified": true
|
|
6128
6181
|
},
|
|
6129
6182
|
{
|
|
@@ -6169,7 +6222,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
6169
6222
|
],
|
|
6170
6223
|
"registeredAt": "2026-05-26",
|
|
6171
6224
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 6.2",
|
|
6172
|
-
"bundledIn": "@cross-deck/web@1.
|
|
6225
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
6173
6226
|
"runtimeVerified": true
|
|
6174
6227
|
},
|
|
6175
6228
|
{
|
|
@@ -6211,7 +6264,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
6211
6264
|
],
|
|
6212
6265
|
"registeredAt": "2026-05-26",
|
|
6213
6266
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.2",
|
|
6214
|
-
"bundledIn": "@cross-deck/web@1.
|
|
6267
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
6215
6268
|
"runtimeVerified": true
|
|
6216
6269
|
},
|
|
6217
6270
|
{
|
|
@@ -6245,7 +6298,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
6245
6298
|
],
|
|
6246
6299
|
"registeredAt": "2026-05-26",
|
|
6247
6300
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.5",
|
|
6248
|
-
"bundledIn": "@cross-deck/web@1.
|
|
6301
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
6249
6302
|
"runtimeVerified": false
|
|
6250
6303
|
}
|
|
6251
6304
|
]);
|