@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.mjs
CHANGED
|
@@ -8,6 +8,8 @@ var CrossdeckError = class _CrossdeckError extends Error {
|
|
|
8
8
|
this.requestId = payload.requestId;
|
|
9
9
|
this.status = payload.status;
|
|
10
10
|
this.retryAfterMs = payload.retryAfterMs;
|
|
11
|
+
this.minVersion = payload.minVersion;
|
|
12
|
+
this.surface = payload.surface;
|
|
11
13
|
Object.setPrototypeOf(this, _CrossdeckError.prototype);
|
|
12
14
|
}
|
|
13
15
|
};
|
|
@@ -28,7 +30,10 @@ async function crossdeckErrorFromResponse(res) {
|
|
|
28
30
|
message: envelope.message ?? `HTTP ${res.status}`,
|
|
29
31
|
requestId: envelope.request_id ?? requestId,
|
|
30
32
|
status: res.status,
|
|
31
|
-
retryAfterMs
|
|
33
|
+
retryAfterMs,
|
|
34
|
+
// PARK metadata, present only on a 426 / sdk_version_unsupported body.
|
|
35
|
+
minVersion: typeof envelope.minVersion === "string" ? envelope.minVersion : void 0,
|
|
36
|
+
surface: typeof envelope.surface === "string" ? envelope.surface : void 0
|
|
32
37
|
});
|
|
33
38
|
}
|
|
34
39
|
return new CrossdeckError({
|
|
@@ -64,7 +69,7 @@ function typeMapForStatus(status) {
|
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
// src/_version.ts
|
|
67
|
-
var SDK_VERSION = "1.
|
|
72
|
+
var SDK_VERSION = "1.8.0";
|
|
68
73
|
var SDK_NAME = "@cross-deck/web";
|
|
69
74
|
|
|
70
75
|
// src/http.ts
|
|
@@ -904,6 +909,18 @@ var EventQueue = class {
|
|
|
904
909
|
this.cancelTimer = null;
|
|
905
910
|
this.firstFlushFired = false;
|
|
906
911
|
this.nextRetryAt = null;
|
|
912
|
+
/**
|
|
913
|
+
* PARK state (HTTP 426 / `sdk_version_unsupported`). Once parked, the
|
|
914
|
+
* queue stops flushing — the server has told us our wire dialect is too
|
|
915
|
+
* old, and retrying the same payload only burns the user's battery and
|
|
916
|
+
* bandwidth and drips pointless rejects into the server logs until the
|
|
917
|
+
* app ships an upgraded SDK. The held events stay durable (persisted)
|
|
918
|
+
* and are delivered by the next boot's rehydrate, post-upgrade. Parked
|
|
919
|
+
* is per-instance: a fresh boot (the upgraded build) starts unparked.
|
|
920
|
+
*/
|
|
921
|
+
this.parked = false;
|
|
922
|
+
/** One developer-facing console warning per instance — never per-event spam. */
|
|
923
|
+
this.parkWarned = false;
|
|
907
924
|
this.retry = new RetryPolicy(cfg.retry ?? {});
|
|
908
925
|
this.persistent = cfg.persistentStore ?? null;
|
|
909
926
|
if (this.persistent) {
|
|
@@ -959,6 +976,7 @@ var EventQueue = class {
|
|
|
959
976
|
* flushes (pagehide / visibilitychange→hidden / beforeunload).
|
|
960
977
|
*/
|
|
961
978
|
async flush(options = {}) {
|
|
979
|
+
if (this.parked) return null;
|
|
962
980
|
let batch;
|
|
963
981
|
let batchId;
|
|
964
982
|
if (this.pendingBatch !== null && this.pendingBatchId !== null) {
|
|
@@ -980,9 +998,13 @@ var EventQueue = class {
|
|
|
980
998
|
const env = this.cfg.envelope();
|
|
981
999
|
const result = await this.cfg.http.request("POST", "/events", {
|
|
982
1000
|
body: {
|
|
983
|
-
//
|
|
984
|
-
//
|
|
985
|
-
//
|
|
1001
|
+
// Event Envelope v1 batch envelope (backend/docs/
|
|
1002
|
+
// event-envelope-spec-v1.md §1). `envelopeVersion` is the
|
|
1003
|
+
// schema/wire version the server parses against ("can I parse
|
|
1004
|
+
// this?") and is DISTINCT from `sdk.version` ("which build is in
|
|
1005
|
+
// the wild?") — two questions, two fields, never conflated. The
|
|
1006
|
+
// backend refuses payloads with a missing/unknown envelopeVersion.
|
|
1007
|
+
envelopeVersion: 1,
|
|
986
1008
|
appId: env.appId,
|
|
987
1009
|
environment: env.environment,
|
|
988
1010
|
sdk: env.sdk,
|
|
@@ -1006,6 +1028,29 @@ var EventQueue = class {
|
|
|
1006
1028
|
} catch (err) {
|
|
1007
1029
|
const message = err instanceof Error ? err.message : String(err);
|
|
1008
1030
|
this.lastError = message;
|
|
1031
|
+
if (isVersionRejected(err)) {
|
|
1032
|
+
this.parked = true;
|
|
1033
|
+
this.buffer = [...batch, ...this.buffer];
|
|
1034
|
+
if (this.buffer.length > HARD_BUFFER_CAP) {
|
|
1035
|
+
const overflow = this.buffer.length - HARD_BUFFER_CAP;
|
|
1036
|
+
this.buffer.splice(0, overflow);
|
|
1037
|
+
this.dropped += overflow;
|
|
1038
|
+
}
|
|
1039
|
+
this.pendingBatch = null;
|
|
1040
|
+
this.pendingBatchId = null;
|
|
1041
|
+
this.inFlight -= batch.length;
|
|
1042
|
+
this.persistAll();
|
|
1043
|
+
this.cfg.onBufferChange?.(this.buffer.length);
|
|
1044
|
+
const minVersion = versionRejectionFloor(err);
|
|
1045
|
+
if (!this.parkWarned) {
|
|
1046
|
+
this.parkWarned = true;
|
|
1047
|
+
console.warn(
|
|
1048
|
+
`[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.`
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
this.cfg.onParked?.({ minVersion, surface: versionRejectionSurface(err) });
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1009
1054
|
if (isPermanent4xx(err)) {
|
|
1010
1055
|
const droppedCount = batch.length;
|
|
1011
1056
|
this.pendingBatch = null;
|
|
@@ -1124,8 +1169,22 @@ function isPermanent4xx(err) {
|
|
|
1124
1169
|
if (typeof status !== "number" || !Number.isFinite(status)) return false;
|
|
1125
1170
|
if (status < 400 || status >= 500) return false;
|
|
1126
1171
|
if (status === 408 || status === 429) return false;
|
|
1172
|
+
if (status === 426) return false;
|
|
1127
1173
|
return true;
|
|
1128
1174
|
}
|
|
1175
|
+
function isVersionRejected(err) {
|
|
1176
|
+
if (!err || typeof err !== "object") return false;
|
|
1177
|
+
if (err.status === 426) return true;
|
|
1178
|
+
return err.code === "sdk_version_unsupported";
|
|
1179
|
+
}
|
|
1180
|
+
function versionRejectionFloor(err) {
|
|
1181
|
+
const v = err?.minVersion;
|
|
1182
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1183
|
+
}
|
|
1184
|
+
function versionRejectionSurface(err) {
|
|
1185
|
+
const v = err?.surface;
|
|
1186
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1187
|
+
}
|
|
1129
1188
|
function defaultScheduler(fn, ms) {
|
|
1130
1189
|
const id = setTimeout(fn, ms);
|
|
1131
1190
|
if (typeof id.unref === "function") {
|
|
@@ -1437,6 +1496,15 @@ var AutoTracker = class {
|
|
|
1437
1496
|
this.cleanups = [];
|
|
1438
1497
|
/** Last time we flushed lastActivityAt to storage (throttle gate). */
|
|
1439
1498
|
this.lastPersistAt = 0;
|
|
1499
|
+
/**
|
|
1500
|
+
* Event Envelope v1 §3 — per-session monotonic sequence counter.
|
|
1501
|
+
* The single owner of the seq counter lives here, alongside all other
|
|
1502
|
+
* session state. Incremented atomically at track() time (via nextSeq()),
|
|
1503
|
+
* reset to 0 whenever a new session boundary is crossed (startNewSession
|
|
1504
|
+
* path + resetSession). Persists across background/foreground within a
|
|
1505
|
+
* session — only a real session boundary (not a tab hide/show) resets it.
|
|
1506
|
+
*/
|
|
1507
|
+
this._sessionSeq = 0;
|
|
1440
1508
|
/**
|
|
1441
1509
|
* Stable per-page-view identifier. Minted at every `page.viewed`
|
|
1442
1510
|
* emission and attached to every subsequent event until the next
|
|
@@ -1521,6 +1589,20 @@ var AutoTracker = class {
|
|
|
1521
1589
|
get currentAcquisition() {
|
|
1522
1590
|
return this.session?.acquisition ?? EMPTY_ACQUISITION;
|
|
1523
1591
|
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Event Envelope v1 §3 — return the next seq value for the current session
|
|
1594
|
+
* and advance the counter. Called SYNCHRONOUSLY at track() time, before any
|
|
1595
|
+
* async dispatch, so the seq assignment order is deterministic (call order,
|
|
1596
|
+
* not scheduler luck). The counter is reset to 0 at every session boundary;
|
|
1597
|
+
* it is NOT reset by tab hide/show (spec §3 clause 1: backgrounding does not
|
|
1598
|
+
* reset seq). When no session is active (Node, before init, after uninstall),
|
|
1599
|
+
* returns 0 and leaves the counter unchanged — fallback, not an error.
|
|
1600
|
+
*/
|
|
1601
|
+
nextSeq() {
|
|
1602
|
+
const seq = this._sessionSeq;
|
|
1603
|
+
this._sessionSeq += 1;
|
|
1604
|
+
return seq;
|
|
1605
|
+
}
|
|
1524
1606
|
// ---------- sessions ----------
|
|
1525
1607
|
installSessionTracking() {
|
|
1526
1608
|
const now = Date.now();
|
|
@@ -1572,6 +1654,7 @@ var AutoTracker = class {
|
|
|
1572
1654
|
}
|
|
1573
1655
|
startNewSession() {
|
|
1574
1656
|
const now = Date.now();
|
|
1657
|
+
this._sessionSeq = 0;
|
|
1575
1658
|
return {
|
|
1576
1659
|
sessionId: mintSessionId(),
|
|
1577
1660
|
startedAt: now,
|
|
@@ -2300,6 +2383,40 @@ function writeJson(storage, key, value) {
|
|
|
2300
2383
|
}
|
|
2301
2384
|
}
|
|
2302
2385
|
|
|
2386
|
+
// src/internal-opt-out.ts
|
|
2387
|
+
var INTERNAL_OPT_OUT_PROPERTY = "$crossdeck_internal";
|
|
2388
|
+
var STORAGE_KEY = "crossdeck.internalOptOut";
|
|
2389
|
+
function localStore() {
|
|
2390
|
+
try {
|
|
2391
|
+
return typeof localStorage !== "undefined" ? localStorage : null;
|
|
2392
|
+
} catch {
|
|
2393
|
+
return null;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
function processInternalOptOutUrl() {
|
|
2397
|
+
try {
|
|
2398
|
+
const search = typeof location !== "undefined" ? location.search : "";
|
|
2399
|
+
const params = new URLSearchParams(search || "");
|
|
2400
|
+
if (!params.has("crossdeck_internal")) return;
|
|
2401
|
+
const store = localStore();
|
|
2402
|
+
if (!store) return;
|
|
2403
|
+
const v = params.get("crossdeck_internal");
|
|
2404
|
+
if (v === "1" || v === "true") {
|
|
2405
|
+
store.setItem(STORAGE_KEY, "1");
|
|
2406
|
+
} else if (v === "0" || v === "false") {
|
|
2407
|
+
store.removeItem(STORAGE_KEY);
|
|
2408
|
+
}
|
|
2409
|
+
} catch {
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
function isInternalOptOut() {
|
|
2413
|
+
try {
|
|
2414
|
+
return localStore()?.getItem(STORAGE_KEY) === "1";
|
|
2415
|
+
} catch {
|
|
2416
|
+
return false;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2303
2420
|
// src/web-vitals.ts
|
|
2304
2421
|
var WebVitalsTracker = class {
|
|
2305
2422
|
constructor(cfg, report) {
|
|
@@ -2808,6 +2925,13 @@ var CROSSDECK_ERROR_CODES = Object.freeze([
|
|
|
2808
2925
|
description: "A field is present but the value failed validation (wrong shape, wrong length, wrong enum value).",
|
|
2809
2926
|
resolution: "Read error.message for the specific field + reason. SDK-managed call sites should never emit this \u2014 file a bug if you do.",
|
|
2810
2927
|
retryable: false
|
|
2928
|
+
},
|
|
2929
|
+
{
|
|
2930
|
+
code: "sdk_version_unsupported",
|
|
2931
|
+
type: "version_error",
|
|
2932
|
+
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.",
|
|
2933
|
+
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/.",
|
|
2934
|
+
retryable: false
|
|
2811
2935
|
}
|
|
2812
2936
|
]);
|
|
2813
2937
|
function getErrorCode(code) {
|
|
@@ -2830,15 +2954,6 @@ var VerifierReporter = class {
|
|
|
2830
2954
|
constructor(ctx) {
|
|
2831
2955
|
this.ctx = ctx;
|
|
2832
2956
|
this.reentrancyDepth = 0;
|
|
2833
|
-
/** The live verifier set — attached by the SDK right after init so the
|
|
2834
|
-
* emit path can schema-lock its OWN outgoing telemetry against the real
|
|
2835
|
-
* payload. Null until attached (boot-time reports still work without it). */
|
|
2836
|
-
this.verifiers = null;
|
|
2837
|
-
}
|
|
2838
|
-
/** Hand the reporter the live verifier set so `reportFail` can run the
|
|
2839
|
-
* `onReportContractFailure` hook on the exact payload it's about to emit. */
|
|
2840
|
-
attachVerifiers(verifiers) {
|
|
2841
|
-
this.verifiers = verifiers;
|
|
2842
2957
|
}
|
|
2843
2958
|
/**
|
|
2844
2959
|
* Report a verifier result. Pass `operation` for hot-path results
|
|
@@ -2867,7 +2982,7 @@ var VerifierReporter = class {
|
|
|
2867
2982
|
if (this.reentrancyDepth > 0) return;
|
|
2868
2983
|
this.reentrancyDepth += 1;
|
|
2869
2984
|
try {
|
|
2870
|
-
|
|
2985
|
+
this.ctx.emitTelemetry({
|
|
2871
2986
|
contract_id: result.contractId,
|
|
2872
2987
|
sdk_version: this.ctx.sdkVersion,
|
|
2873
2988
|
sdk_platform: "web",
|
|
@@ -2875,13 +2990,7 @@ var VerifierReporter = class {
|
|
|
2875
2990
|
run_context: this.ctx.runContext,
|
|
2876
2991
|
run_id: this.ctx.runId,
|
|
2877
2992
|
verification_phase: phase
|
|
2878
|
-
};
|
|
2879
|
-
if (this.verifiers) {
|
|
2880
|
-
runOnReportContractFailure(this.verifiers, this, this.ctx, {
|
|
2881
|
-
outgoingPayload: payload
|
|
2882
|
-
});
|
|
2883
|
-
}
|
|
2884
|
-
this.ctx.emitTelemetry(payload);
|
|
2993
|
+
});
|
|
2885
2994
|
} finally {
|
|
2886
2995
|
this.reentrancyDepth -= 1;
|
|
2887
2996
|
}
|
|
@@ -3171,31 +3280,9 @@ var VERIFIER_FLUSH_INTERVAL_PARITY = {
|
|
|
3171
3280
|
"eventFlushIntervalMs default = 2000ms (Web/Node/RN/Swift/Android parity)",
|
|
3172
3281
|
nowMs() - t0
|
|
3173
3282
|
);
|
|
3174
|
-
},
|
|
3175
|
-
// Hot-path hook — on every real track() we re-affirm the LIVE configured
|
|
3176
|
-
// flush interval (carried on the observation), so the parity guarantee is
|
|
3177
|
-
// verified against the running SDK in the customer flow, not just a
|
|
3178
|
-
// canonical constant at boot. It can't change per-event, but observing it
|
|
3179
|
-
// per-event is what makes the contract genuinely runtime-verified.
|
|
3180
|
-
hooks: {
|
|
3181
|
-
onTrack(obs) {
|
|
3182
|
-
const t0 = nowMs();
|
|
3183
|
-
const CANONICAL_DEFAULT_MS = 2e3;
|
|
3184
|
-
const ms = obs.flushIntervalMs;
|
|
3185
|
-
if (typeof ms !== "number" || ms < 100 || ms > 6e4) {
|
|
3186
|
-
return fail(
|
|
3187
|
-
"flush-interval-parity",
|
|
3188
|
-
`live eventFlushIntervalMs=${ms} outside reasonable bounds [100, 60000]`,
|
|
3189
|
-
nowMs() - t0
|
|
3190
|
-
);
|
|
3191
|
-
}
|
|
3192
|
-
return pass(
|
|
3193
|
-
"flush-interval-parity",
|
|
3194
|
-
ms === CANONICAL_DEFAULT_MS ? "live eventFlushIntervalMs = 2000ms (canonical parity value)" : `live eventFlushIntervalMs = ${ms}ms (permitted override; canonical default 2000ms)`,
|
|
3195
|
-
nowMs() - t0
|
|
3196
|
-
);
|
|
3197
|
-
}
|
|
3198
3283
|
}
|
|
3284
|
+
// No hot-path hook — the flush interval is set once at start() and
|
|
3285
|
+
// never changes per-operation.
|
|
3199
3286
|
};
|
|
3200
3287
|
function buildFlushIntervalVerifier(configuredIntervalMs) {
|
|
3201
3288
|
return {
|
|
@@ -3322,26 +3409,6 @@ var CONTRACT_FAILED_FORBIDDEN_FIELDS = [
|
|
|
3322
3409
|
"referrer",
|
|
3323
3410
|
"deviceId"
|
|
3324
3411
|
];
|
|
3325
|
-
function contractFailedSchemaViolation(keys) {
|
|
3326
|
-
const allowed = /* @__PURE__ */ new Set([
|
|
3327
|
-
...CONTRACT_FAILED_REQUIRED_FIELDS,
|
|
3328
|
-
...CONTRACT_FAILED_OPTIONAL_FIELDS
|
|
3329
|
-
]);
|
|
3330
|
-
const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
|
|
3331
|
-
for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
|
|
3332
|
-
if (!keys.includes(required)) return `missing required field: ${required}`;
|
|
3333
|
-
}
|
|
3334
|
-
for (const k of keys) {
|
|
3335
|
-
if (forbidden.has(k)) return `forbidden field on wire: ${k}`;
|
|
3336
|
-
}
|
|
3337
|
-
for (const k of keys) {
|
|
3338
|
-
if (!allowed.has(k)) {
|
|
3339
|
-
return `unrecognised field on wire: ${k} (not in required \u222A optional)`;
|
|
3340
|
-
}
|
|
3341
|
-
}
|
|
3342
|
-
return null;
|
|
3343
|
-
}
|
|
3344
|
-
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`;
|
|
3345
3412
|
var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
|
|
3346
3413
|
contractId: "contract-failed-payload-schema-lock",
|
|
3347
3414
|
bootTest() {
|
|
@@ -3356,35 +3423,43 @@ var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
|
|
|
3356
3423
|
verification_phase: "boot"
|
|
3357
3424
|
};
|
|
3358
3425
|
const keys = Object.keys(syntheticPayload);
|
|
3359
|
-
const
|
|
3360
|
-
|
|
3361
|
-
|
|
3426
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
3427
|
+
...CONTRACT_FAILED_REQUIRED_FIELDS,
|
|
3428
|
+
...CONTRACT_FAILED_OPTIONAL_FIELDS
|
|
3429
|
+
]);
|
|
3430
|
+
const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
|
|
3431
|
+
for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
|
|
3432
|
+
if (!keys.includes(required)) {
|
|
3433
|
+
return fail(
|
|
3434
|
+
"contract-failed-payload-schema-lock",
|
|
3435
|
+
`missing required field: ${required}`,
|
|
3436
|
+
nowMs() - t0
|
|
3437
|
+
);
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
for (const k of keys) {
|
|
3441
|
+
if (forbidden.has(k)) {
|
|
3442
|
+
return fail(
|
|
3443
|
+
"contract-failed-payload-schema-lock",
|
|
3444
|
+
`forbidden field on wire: ${k}`,
|
|
3445
|
+
nowMs() - t0
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
for (const k of keys) {
|
|
3450
|
+
if (!allowed.has(k)) {
|
|
3451
|
+
return fail(
|
|
3452
|
+
"contract-failed-payload-schema-lock",
|
|
3453
|
+
`unrecognised field on wire: ${k} (not in required \u222A optional)`,
|
|
3454
|
+
nowMs() - t0
|
|
3455
|
+
);
|
|
3456
|
+
}
|
|
3362
3457
|
}
|
|
3363
3458
|
return pass(
|
|
3364
3459
|
"contract-failed-payload-schema-lock",
|
|
3365
|
-
`${keys.length} ${
|
|
3460
|
+
`${keys.length} fields \u2286 required(${CONTRACT_FAILED_REQUIRED_FIELDS.length}) \u222A optional(${CONTRACT_FAILED_OPTIONAL_FIELDS.length}); ${CONTRACT_FAILED_FORBIDDEN_FIELDS.length} forbidden absent`,
|
|
3366
3461
|
nowMs() - t0
|
|
3367
3462
|
);
|
|
3368
|
-
},
|
|
3369
|
-
// Hot-path hook — fires on the REAL reportFail emit (VerifierReporter,
|
|
3370
|
-
// inside the re-entrancy guard) against the exact payload about to go on
|
|
3371
|
-
// the wire. This is the assertion the guard at reportFail was built in
|
|
3372
|
-
// anticipation of: every reliability-channel write is schema-locked in the
|
|
3373
|
-
// field, against real data, not just the synthetic boot mirror.
|
|
3374
|
-
hooks: {
|
|
3375
|
-
onReportContractFailure(obs) {
|
|
3376
|
-
const t0 = nowMs();
|
|
3377
|
-
const keys = Object.keys(obs.outgoingPayload);
|
|
3378
|
-
const violation = contractFailedSchemaViolation(keys);
|
|
3379
|
-
if (violation) {
|
|
3380
|
-
return fail("contract-failed-payload-schema-lock", violation, nowMs() - t0);
|
|
3381
|
-
}
|
|
3382
|
-
return pass(
|
|
3383
|
-
"contract-failed-payload-schema-lock",
|
|
3384
|
-
`${keys.length} ${CONTRACT_FAILED_OK_EVIDENCE}`,
|
|
3385
|
-
nowMs() - t0
|
|
3386
|
-
);
|
|
3387
|
-
}
|
|
3388
3463
|
}
|
|
3389
3464
|
};
|
|
3390
3465
|
var BACKEND_WIRE_CODES = Object.freeze([
|
|
@@ -3402,7 +3477,8 @@ var BACKEND_WIRE_CODES = Object.freeze([
|
|
|
3402
3477
|
"google_not_supported",
|
|
3403
3478
|
"stripe_not_supported",
|
|
3404
3479
|
"missing_required_param",
|
|
3405
|
-
"invalid_param_value"
|
|
3480
|
+
"invalid_param_value",
|
|
3481
|
+
"sdk_version_unsupported"
|
|
3406
3482
|
]);
|
|
3407
3483
|
var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
|
|
3408
3484
|
contractId: "sdk-error-codes-catalogue",
|
|
@@ -3435,39 +3511,6 @@ var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
|
|
|
3435
3511
|
nowMs() - t0
|
|
3436
3512
|
);
|
|
3437
3513
|
}
|
|
3438
|
-
},
|
|
3439
|
-
// Hot-path hook — when the SDK parses a real wire error, assert the code
|
|
3440
|
-
// the customer actually hit carries usable remediation in the shipped
|
|
3441
|
-
// catalogue. Scoped to BACKEND_WIRE_CODES (the contract's set); other
|
|
3442
|
-
// codes are out of scope and pass. This turns the catalogue from a
|
|
3443
|
-
// boot-only completeness claim into a per-error field assertion: a backend
|
|
3444
|
-
// code that ships without remediation is caught the first time a customer
|
|
3445
|
-
// hits it, not only if the off-by-default boot self-test ran.
|
|
3446
|
-
hooks: {
|
|
3447
|
-
onErrorParse(obs) {
|
|
3448
|
-
const t0 = nowMs();
|
|
3449
|
-
const code = obs.errorCode;
|
|
3450
|
-
if (!code || !BACKEND_WIRE_CODES.includes(code)) {
|
|
3451
|
-
return pass(
|
|
3452
|
-
"sdk-error-codes-catalogue",
|
|
3453
|
-
`wire code "${code || "(none)"}" is out of the backend-catalogue scope`,
|
|
3454
|
-
nowMs() - t0
|
|
3455
|
-
);
|
|
3456
|
-
}
|
|
3457
|
-
const entry = getErrorCode(code);
|
|
3458
|
-
if (!entry || !entry.description || entry.description.trim().length === 0 || !entry.resolution || entry.resolution.trim().length === 0) {
|
|
3459
|
-
return fail(
|
|
3460
|
-
"sdk-error-codes-catalogue",
|
|
3461
|
-
`backend wire code "${code}" hit in the field has no description+resolution in the shipped catalogue`,
|
|
3462
|
-
nowMs() - t0
|
|
3463
|
-
);
|
|
3464
|
-
}
|
|
3465
|
-
return pass(
|
|
3466
|
-
"sdk-error-codes-catalogue",
|
|
3467
|
-
`backend wire code "${code}" carries description + resolution`,
|
|
3468
|
-
nowMs() - t0
|
|
3469
|
-
);
|
|
3470
|
-
}
|
|
3471
3514
|
}
|
|
3472
3515
|
};
|
|
3473
3516
|
var STATIC_VERIFIERS = Object.freeze([
|
|
@@ -3591,24 +3634,6 @@ function runOnErrorParse(verifiers, reporter, ctx, obs) {
|
|
|
3591
3634
|
reporter.report(result, "hot_path", "errorParse");
|
|
3592
3635
|
}
|
|
3593
3636
|
}
|
|
3594
|
-
function runOnReportContractFailure(verifiers, reporter, ctx, obs) {
|
|
3595
|
-
if (ctx.disableContractAssertions) return;
|
|
3596
|
-
for (const verifier of verifiers) {
|
|
3597
|
-
const hook = verifier.hooks?.onReportContractFailure;
|
|
3598
|
-
if (!hook) continue;
|
|
3599
|
-
let result;
|
|
3600
|
-
try {
|
|
3601
|
-
result = hook(obs);
|
|
3602
|
-
} catch (err) {
|
|
3603
|
-
result = fail(
|
|
3604
|
-
verifier.contractId,
|
|
3605
|
-
`hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
|
|
3606
|
-
0
|
|
3607
|
-
);
|
|
3608
|
-
}
|
|
3609
|
-
reporter.report(result, "hot_path", "reportContractFailure");
|
|
3610
|
-
}
|
|
3611
|
-
}
|
|
3612
3637
|
function defaultDebugModeFlag() {
|
|
3613
3638
|
try {
|
|
3614
3639
|
if (typeof process !== "undefined" && process.env) {
|
|
@@ -3760,18 +3785,18 @@ function isInAppFrame(filename) {
|
|
|
3760
3785
|
if (/\/crossdeck\.umd\.min\.js$/.test(filename)) return false;
|
|
3761
3786
|
return true;
|
|
3762
3787
|
}
|
|
3763
|
-
function fingerprintError(message, frames,
|
|
3788
|
+
function fingerprintError(message, frames, location2) {
|
|
3764
3789
|
const inAppFrames = frames.filter((f) => f.in_app).slice(0, 3);
|
|
3765
3790
|
const parts = [
|
|
3766
3791
|
(message || "").slice(0, 200),
|
|
3767
3792
|
...inAppFrames.map((f) => `${f.function}@${f.filename}:${f.lineno}`)
|
|
3768
3793
|
];
|
|
3769
|
-
if (inAppFrames.length === 0 &&
|
|
3794
|
+
if (inAppFrames.length === 0 && location2) {
|
|
3770
3795
|
const loc = [
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3796
|
+
location2.errorType ?? "",
|
|
3797
|
+
location2.filename ?? "",
|
|
3798
|
+
location2.lineno ?? "",
|
|
3799
|
+
location2.colno ?? ""
|
|
3775
3800
|
].join(":");
|
|
3776
3801
|
if (loc !== ":::") parts.push(loc);
|
|
3777
3802
|
}
|
|
@@ -4441,9 +4466,6 @@ var CrossdeckClient = class {
|
|
|
4441
4466
|
this.verifiers = null;
|
|
4442
4467
|
this.verifierReporter = null;
|
|
4443
4468
|
this.verifierCtx = null;
|
|
4444
|
-
// The live configured event-flush interval, surfaced to the track-path
|
|
4445
|
-
// verifier so flush-interval-parity validates the real cadence per event.
|
|
4446
|
-
this.flushIntervalMs = 2e3;
|
|
4447
4469
|
}
|
|
4448
4470
|
/**
|
|
4449
4471
|
* Boot the SDK. Idempotent — calling init twice with the same options
|
|
@@ -4613,6 +4635,13 @@ var CrossdeckClient = class {
|
|
|
4613
4635
|
headline,
|
|
4614
4636
|
{ ...info }
|
|
4615
4637
|
);
|
|
4638
|
+
},
|
|
4639
|
+
onParked: (info) => {
|
|
4640
|
+
debug.emit(
|
|
4641
|
+
"sdk.parked",
|
|
4642
|
+
`[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.`,
|
|
4643
|
+
{ ...info }
|
|
4644
|
+
);
|
|
4616
4645
|
}
|
|
4617
4646
|
});
|
|
4618
4647
|
const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
|
|
@@ -4620,6 +4649,7 @@ var CrossdeckClient = class {
|
|
|
4620
4649
|
persistIdentity ? effectiveStorage : new MemoryStorage(),
|
|
4621
4650
|
opts.storagePrefix
|
|
4622
4651
|
);
|
|
4652
|
+
processInternalOptOutUrl();
|
|
4623
4653
|
const consent = new ConsentManager({ respectDnt: options.respectDnt === true });
|
|
4624
4654
|
if (consent.isDntDenied) {
|
|
4625
4655
|
debug.emit(
|
|
@@ -4723,8 +4753,6 @@ var CrossdeckClient = class {
|
|
|
4723
4753
|
this.verifierReporter = new VerifierReporter(this.verifierCtx);
|
|
4724
4754
|
const flushVerifier = buildFlushIntervalVerifier(opts.eventFlushIntervalMs);
|
|
4725
4755
|
this.verifiers = Object.freeze([...STATIC_VERIFIERS, flushVerifier]);
|
|
4726
|
-
this.flushIntervalMs = opts.eventFlushIntervalMs;
|
|
4727
|
-
this.verifierReporter.attachVerifiers(this.verifiers);
|
|
4728
4756
|
if (localDevMode) {
|
|
4729
4757
|
const verifyAtBootLocal = options.verifyContractsAtBoot ?? debugDefault;
|
|
4730
4758
|
if (verifyAtBootLocal && this.verifierReporter) {
|
|
@@ -5275,8 +5303,27 @@ var CrossdeckClient = class {
|
|
|
5275
5303
|
);
|
|
5276
5304
|
}
|
|
5277
5305
|
}
|
|
5306
|
+
const seq = s.autoTracker?.nextSeq() ?? 0;
|
|
5307
|
+
const occurrenceTimestamp = Date.now();
|
|
5278
5308
|
s.autoTracker?.markActivity();
|
|
5279
|
-
const
|
|
5309
|
+
const wireContext = {};
|
|
5310
|
+
const di = s.deviceInfo;
|
|
5311
|
+
if (di.os) wireContext.os = di.os;
|
|
5312
|
+
if (di.osVersion) wireContext.osVersion = di.osVersion;
|
|
5313
|
+
const appVer = s.options.appVersion;
|
|
5314
|
+
if (appVer) wireContext.appVersion = appVer;
|
|
5315
|
+
wireContext.sdkName = SDK_NAME;
|
|
5316
|
+
wireContext.sdkVersion = s.options.sdkVersion;
|
|
5317
|
+
if (di.locale) wireContext.locale = di.locale;
|
|
5318
|
+
if (di.timezone) wireContext.timezone = di.timezone;
|
|
5319
|
+
if (di.browser) wireContext.browser = di.browser;
|
|
5320
|
+
if (di.browserVersion) wireContext.browserVersion = di.browserVersion;
|
|
5321
|
+
const enriched = {};
|
|
5322
|
+
if (di.screenWidth !== void 0) enriched.screenWidth = di.screenWidth;
|
|
5323
|
+
if (di.screenHeight !== void 0) enriched.screenHeight = di.screenHeight;
|
|
5324
|
+
if (di.viewportWidth !== void 0) enriched.viewportWidth = di.viewportWidth;
|
|
5325
|
+
if (di.viewportHeight !== void 0) enriched.viewportHeight = di.viewportHeight;
|
|
5326
|
+
if (di.devicePixelRatio !== void 0) enriched.devicePixelRatio = di.devicePixelRatio;
|
|
5280
5327
|
const sessionId = s.autoTracker?.currentSessionId;
|
|
5281
5328
|
if (sessionId) enriched.sessionId = sessionId;
|
|
5282
5329
|
const pageviewId = s.autoTracker?.currentPageviewId;
|
|
@@ -5299,17 +5346,24 @@ var CrossdeckClient = class {
|
|
|
5299
5346
|
}
|
|
5300
5347
|
}
|
|
5301
5348
|
const supers = s.superProps.getSuperProperties();
|
|
5302
|
-
Object.
|
|
5349
|
+
for (const k of Object.keys(supers)) {
|
|
5350
|
+
if (!(k in enriched)) enriched[k] = supers[k];
|
|
5351
|
+
}
|
|
5303
5352
|
const groupIds = s.superProps.getGroupIds();
|
|
5304
5353
|
if (Object.keys(groupIds).length > 0) {
|
|
5305
5354
|
enriched.$groups = groupIds;
|
|
5306
5355
|
}
|
|
5307
5356
|
Object.assign(enriched, validation.properties);
|
|
5357
|
+
if (isInternalOptOut()) {
|
|
5358
|
+
enriched[INTERNAL_OPT_OUT_PROPERTY] = true;
|
|
5359
|
+
}
|
|
5308
5360
|
const finalProperties = s.scrubPii ? scrubPiiFromProperties(enriched) : enriched;
|
|
5309
5361
|
const event = {
|
|
5310
5362
|
eventId: this.mintEventId(),
|
|
5311
5363
|
name,
|
|
5312
|
-
timestamp:
|
|
5364
|
+
timestamp: occurrenceTimestamp,
|
|
5365
|
+
seq,
|
|
5366
|
+
context: wireContext,
|
|
5313
5367
|
properties: finalProperties
|
|
5314
5368
|
};
|
|
5315
5369
|
Object.assign(event, this.identityHintForEvent());
|
|
@@ -5319,8 +5373,7 @@ var CrossdeckClient = class {
|
|
|
5319
5373
|
callerProperties: validation.properties,
|
|
5320
5374
|
superProperties: supers,
|
|
5321
5375
|
deviceProperties: s.deviceInfo,
|
|
5322
|
-
mergedProperties: enriched
|
|
5323
|
-
flushIntervalMs: this.flushIntervalMs
|
|
5376
|
+
mergedProperties: enriched
|
|
5324
5377
|
});
|
|
5325
5378
|
}
|
|
5326
5379
|
if (!isError && !isWebVital) {
|
|
@@ -5665,8 +5718,8 @@ function installUnloadFlush(onUnload) {
|
|
|
5665
5718
|
}
|
|
5666
5719
|
|
|
5667
5720
|
// src/_contracts-bundled.ts
|
|
5668
|
-
var BUNDLED_IN = "@cross-deck/web@1.
|
|
5669
|
-
var SDK_VERSION2 = "1.
|
|
5721
|
+
var BUNDLED_IN = "@cross-deck/web@1.8.0";
|
|
5722
|
+
var SDK_VERSION2 = "1.8.0";
|
|
5670
5723
|
var BUNDLED_CONTRACTS = Object.freeze([
|
|
5671
5724
|
{
|
|
5672
5725
|
"id": "contract-failed-payload-schema-lock",
|
|
@@ -5788,7 +5841,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5788
5841
|
"legal/security/index.html#diagnostic",
|
|
5789
5842
|
"legal/sdk-data/index.html#b-diagnostic"
|
|
5790
5843
|
],
|
|
5791
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5844
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
5792
5845
|
"runtimeVerified": true
|
|
5793
5846
|
},
|
|
5794
5847
|
{
|
|
@@ -5828,7 +5881,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5828
5881
|
],
|
|
5829
5882
|
"registeredAt": "2026-05-26",
|
|
5830
5883
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 8 (codifies existing contract)",
|
|
5831
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5884
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
5832
5885
|
"runtimeVerified": true
|
|
5833
5886
|
},
|
|
5834
5887
|
{
|
|
@@ -5874,7 +5927,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5874
5927
|
],
|
|
5875
5928
|
"registeredAt": "2026-05-26",
|
|
5876
5929
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.3",
|
|
5877
|
-
"bundledIn": "@cross-deck/web@1.
|
|
5930
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
5878
5931
|
"runtimeVerified": true
|
|
5879
5932
|
},
|
|
5880
5933
|
{
|
|
@@ -5980,7 +6033,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
5980
6033
|
],
|
|
5981
6034
|
"registeredAt": "2026-05-26",
|
|
5982
6035
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 2.2.a + 2.2.b + 2.2.c",
|
|
5983
|
-
"bundledIn": "@cross-deck/web@1.
|
|
6036
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
5984
6037
|
"runtimeVerified": true
|
|
5985
6038
|
},
|
|
5986
6039
|
{
|
|
@@ -6008,7 +6061,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
6008
6061
|
],
|
|
6009
6062
|
"registeredAt": "2026-05-26",
|
|
6010
6063
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 5.5",
|
|
6011
|
-
"bundledIn": "@cross-deck/web@1.
|
|
6064
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
6012
6065
|
"runtimeVerified": false
|
|
6013
6066
|
},
|
|
6014
6067
|
{
|
|
@@ -6088,7 +6141,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
6088
6141
|
],
|
|
6089
6142
|
"registeredAt": "2026-05-26",
|
|
6090
6143
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 1.3 (web/RN) + dogfood-gap fix (swift + android)",
|
|
6091
|
-
"bundledIn": "@cross-deck/web@1.
|
|
6144
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
6092
6145
|
"runtimeVerified": true
|
|
6093
6146
|
},
|
|
6094
6147
|
{
|
|
@@ -6134,7 +6187,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
6134
6187
|
],
|
|
6135
6188
|
"registeredAt": "2026-05-26",
|
|
6136
6189
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 6.2",
|
|
6137
|
-
"bundledIn": "@cross-deck/web@1.
|
|
6190
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
6138
6191
|
"runtimeVerified": true
|
|
6139
6192
|
},
|
|
6140
6193
|
{
|
|
@@ -6176,7 +6229,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
6176
6229
|
],
|
|
6177
6230
|
"registeredAt": "2026-05-26",
|
|
6178
6231
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.2",
|
|
6179
|
-
"bundledIn": "@cross-deck/web@1.
|
|
6232
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
6180
6233
|
"runtimeVerified": true
|
|
6181
6234
|
},
|
|
6182
6235
|
{
|
|
@@ -6210,7 +6263,7 @@ var BUNDLED_CONTRACTS = Object.freeze([
|
|
|
6210
6263
|
],
|
|
6211
6264
|
"registeredAt": "2026-05-26",
|
|
6212
6265
|
"firstRegisteredIn": "bank-grade reconciliation v1.4.0 \u2014 phase 3.5",
|
|
6213
|
-
"bundledIn": "@cross-deck/web@1.
|
|
6266
|
+
"bundledIn": "@cross-deck/web@1.8.0",
|
|
6214
6267
|
"runtimeVerified": false
|
|
6215
6268
|
}
|
|
6216
6269
|
]);
|