@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/react.cjs
CHANGED
|
@@ -37,6 +37,8 @@ var CrossdeckError = class _CrossdeckError extends Error {
|
|
|
37
37
|
this.requestId = payload.requestId;
|
|
38
38
|
this.status = payload.status;
|
|
39
39
|
this.retryAfterMs = payload.retryAfterMs;
|
|
40
|
+
this.minVersion = payload.minVersion;
|
|
41
|
+
this.surface = payload.surface;
|
|
40
42
|
Object.setPrototypeOf(this, _CrossdeckError.prototype);
|
|
41
43
|
}
|
|
42
44
|
};
|
|
@@ -57,7 +59,10 @@ async function crossdeckErrorFromResponse(res) {
|
|
|
57
59
|
message: envelope.message ?? `HTTP ${res.status}`,
|
|
58
60
|
requestId: envelope.request_id ?? requestId,
|
|
59
61
|
status: res.status,
|
|
60
|
-
retryAfterMs
|
|
62
|
+
retryAfterMs,
|
|
63
|
+
// PARK metadata, present only on a 426 / sdk_version_unsupported body.
|
|
64
|
+
minVersion: typeof envelope.minVersion === "string" ? envelope.minVersion : void 0,
|
|
65
|
+
surface: typeof envelope.surface === "string" ? envelope.surface : void 0
|
|
61
66
|
});
|
|
62
67
|
}
|
|
63
68
|
return new CrossdeckError({
|
|
@@ -93,7 +98,7 @@ function typeMapForStatus(status) {
|
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
// src/_version.ts
|
|
96
|
-
var SDK_VERSION = "1.
|
|
101
|
+
var SDK_VERSION = "1.8.0";
|
|
97
102
|
var SDK_NAME = "@cross-deck/web";
|
|
98
103
|
|
|
99
104
|
// src/http.ts
|
|
@@ -933,6 +938,18 @@ var EventQueue = class {
|
|
|
933
938
|
this.cancelTimer = null;
|
|
934
939
|
this.firstFlushFired = false;
|
|
935
940
|
this.nextRetryAt = null;
|
|
941
|
+
/**
|
|
942
|
+
* PARK state (HTTP 426 / `sdk_version_unsupported`). Once parked, the
|
|
943
|
+
* queue stops flushing — the server has told us our wire dialect is too
|
|
944
|
+
* old, and retrying the same payload only burns the user's battery and
|
|
945
|
+
* bandwidth and drips pointless rejects into the server logs until the
|
|
946
|
+
* app ships an upgraded SDK. The held events stay durable (persisted)
|
|
947
|
+
* and are delivered by the next boot's rehydrate, post-upgrade. Parked
|
|
948
|
+
* is per-instance: a fresh boot (the upgraded build) starts unparked.
|
|
949
|
+
*/
|
|
950
|
+
this.parked = false;
|
|
951
|
+
/** One developer-facing console warning per instance — never per-event spam. */
|
|
952
|
+
this.parkWarned = false;
|
|
936
953
|
this.retry = new RetryPolicy(cfg.retry ?? {});
|
|
937
954
|
this.persistent = cfg.persistentStore ?? null;
|
|
938
955
|
if (this.persistent) {
|
|
@@ -988,6 +1005,7 @@ var EventQueue = class {
|
|
|
988
1005
|
* flushes (pagehide / visibilitychange→hidden / beforeunload).
|
|
989
1006
|
*/
|
|
990
1007
|
async flush(options = {}) {
|
|
1008
|
+
if (this.parked) return null;
|
|
991
1009
|
let batch;
|
|
992
1010
|
let batchId;
|
|
993
1011
|
if (this.pendingBatch !== null && this.pendingBatchId !== null) {
|
|
@@ -1009,9 +1027,13 @@ var EventQueue = class {
|
|
|
1009
1027
|
const env = this.cfg.envelope();
|
|
1010
1028
|
const result = await this.cfg.http.request("POST", "/events", {
|
|
1011
1029
|
body: {
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
1030
|
+
// Event Envelope v1 batch envelope (backend/docs/
|
|
1031
|
+
// event-envelope-spec-v1.md §1). `envelopeVersion` is the
|
|
1032
|
+
// schema/wire version the server parses against ("can I parse
|
|
1033
|
+
// this?") and is DISTINCT from `sdk.version` ("which build is in
|
|
1034
|
+
// the wild?") — two questions, two fields, never conflated. The
|
|
1035
|
+
// backend refuses payloads with a missing/unknown envelopeVersion.
|
|
1036
|
+
envelopeVersion: 1,
|
|
1015
1037
|
appId: env.appId,
|
|
1016
1038
|
environment: env.environment,
|
|
1017
1039
|
sdk: env.sdk,
|
|
@@ -1035,6 +1057,29 @@ var EventQueue = class {
|
|
|
1035
1057
|
} catch (err) {
|
|
1036
1058
|
const message = err instanceof Error ? err.message : String(err);
|
|
1037
1059
|
this.lastError = message;
|
|
1060
|
+
if (isVersionRejected(err)) {
|
|
1061
|
+
this.parked = true;
|
|
1062
|
+
this.buffer = [...batch, ...this.buffer];
|
|
1063
|
+
if (this.buffer.length > HARD_BUFFER_CAP) {
|
|
1064
|
+
const overflow = this.buffer.length - HARD_BUFFER_CAP;
|
|
1065
|
+
this.buffer.splice(0, overflow);
|
|
1066
|
+
this.dropped += overflow;
|
|
1067
|
+
}
|
|
1068
|
+
this.pendingBatch = null;
|
|
1069
|
+
this.pendingBatchId = null;
|
|
1070
|
+
this.inFlight -= batch.length;
|
|
1071
|
+
this.persistAll();
|
|
1072
|
+
this.cfg.onBufferChange?.(this.buffer.length);
|
|
1073
|
+
const minVersion = versionRejectionFloor(err);
|
|
1074
|
+
if (!this.parkWarned) {
|
|
1075
|
+
this.parkWarned = true;
|
|
1076
|
+
console.warn(
|
|
1077
|
+
`[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.`
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
this.cfg.onParked?.({ minVersion, surface: versionRejectionSurface(err) });
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1038
1083
|
if (isPermanent4xx(err)) {
|
|
1039
1084
|
const droppedCount = batch.length;
|
|
1040
1085
|
this.pendingBatch = null;
|
|
@@ -1153,8 +1198,22 @@ function isPermanent4xx(err) {
|
|
|
1153
1198
|
if (typeof status !== "number" || !Number.isFinite(status)) return false;
|
|
1154
1199
|
if (status < 400 || status >= 500) return false;
|
|
1155
1200
|
if (status === 408 || status === 429) return false;
|
|
1201
|
+
if (status === 426) return false;
|
|
1156
1202
|
return true;
|
|
1157
1203
|
}
|
|
1204
|
+
function isVersionRejected(err) {
|
|
1205
|
+
if (!err || typeof err !== "object") return false;
|
|
1206
|
+
if (err.status === 426) return true;
|
|
1207
|
+
return err.code === "sdk_version_unsupported";
|
|
1208
|
+
}
|
|
1209
|
+
function versionRejectionFloor(err) {
|
|
1210
|
+
const v = err?.minVersion;
|
|
1211
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1212
|
+
}
|
|
1213
|
+
function versionRejectionSurface(err) {
|
|
1214
|
+
const v = err?.surface;
|
|
1215
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1216
|
+
}
|
|
1158
1217
|
function defaultScheduler(fn, ms) {
|
|
1159
1218
|
const id = setTimeout(fn, ms);
|
|
1160
1219
|
if (typeof id.unref === "function") {
|
|
@@ -1466,6 +1525,15 @@ var AutoTracker = class {
|
|
|
1466
1525
|
this.cleanups = [];
|
|
1467
1526
|
/** Last time we flushed lastActivityAt to storage (throttle gate). */
|
|
1468
1527
|
this.lastPersistAt = 0;
|
|
1528
|
+
/**
|
|
1529
|
+
* Event Envelope v1 §3 — per-session monotonic sequence counter.
|
|
1530
|
+
* The single owner of the seq counter lives here, alongside all other
|
|
1531
|
+
* session state. Incremented atomically at track() time (via nextSeq()),
|
|
1532
|
+
* reset to 0 whenever a new session boundary is crossed (startNewSession
|
|
1533
|
+
* path + resetSession). Persists across background/foreground within a
|
|
1534
|
+
* session — only a real session boundary (not a tab hide/show) resets it.
|
|
1535
|
+
*/
|
|
1536
|
+
this._sessionSeq = 0;
|
|
1469
1537
|
/**
|
|
1470
1538
|
* Stable per-page-view identifier. Minted at every `page.viewed`
|
|
1471
1539
|
* emission and attached to every subsequent event until the next
|
|
@@ -1550,6 +1618,20 @@ var AutoTracker = class {
|
|
|
1550
1618
|
get currentAcquisition() {
|
|
1551
1619
|
return this.session?.acquisition ?? EMPTY_ACQUISITION;
|
|
1552
1620
|
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Event Envelope v1 §3 — return the next seq value for the current session
|
|
1623
|
+
* and advance the counter. Called SYNCHRONOUSLY at track() time, before any
|
|
1624
|
+
* async dispatch, so the seq assignment order is deterministic (call order,
|
|
1625
|
+
* not scheduler luck). The counter is reset to 0 at every session boundary;
|
|
1626
|
+
* it is NOT reset by tab hide/show (spec §3 clause 1: backgrounding does not
|
|
1627
|
+
* reset seq). When no session is active (Node, before init, after uninstall),
|
|
1628
|
+
* returns 0 and leaves the counter unchanged — fallback, not an error.
|
|
1629
|
+
*/
|
|
1630
|
+
nextSeq() {
|
|
1631
|
+
const seq = this._sessionSeq;
|
|
1632
|
+
this._sessionSeq += 1;
|
|
1633
|
+
return seq;
|
|
1634
|
+
}
|
|
1553
1635
|
// ---------- sessions ----------
|
|
1554
1636
|
installSessionTracking() {
|
|
1555
1637
|
const now = Date.now();
|
|
@@ -1601,6 +1683,7 @@ var AutoTracker = class {
|
|
|
1601
1683
|
}
|
|
1602
1684
|
startNewSession() {
|
|
1603
1685
|
const now = Date.now();
|
|
1686
|
+
this._sessionSeq = 0;
|
|
1604
1687
|
return {
|
|
1605
1688
|
sessionId: mintSessionId(),
|
|
1606
1689
|
startedAt: now,
|
|
@@ -2329,6 +2412,40 @@ function writeJson(storage, key, value) {
|
|
|
2329
2412
|
}
|
|
2330
2413
|
}
|
|
2331
2414
|
|
|
2415
|
+
// src/internal-opt-out.ts
|
|
2416
|
+
var INTERNAL_OPT_OUT_PROPERTY = "$crossdeck_internal";
|
|
2417
|
+
var STORAGE_KEY = "crossdeck.internalOptOut";
|
|
2418
|
+
function localStore() {
|
|
2419
|
+
try {
|
|
2420
|
+
return typeof localStorage !== "undefined" ? localStorage : null;
|
|
2421
|
+
} catch {
|
|
2422
|
+
return null;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
function processInternalOptOutUrl() {
|
|
2426
|
+
try {
|
|
2427
|
+
const search = typeof location !== "undefined" ? location.search : "";
|
|
2428
|
+
const params = new URLSearchParams(search || "");
|
|
2429
|
+
if (!params.has("crossdeck_internal")) return;
|
|
2430
|
+
const store = localStore();
|
|
2431
|
+
if (!store) return;
|
|
2432
|
+
const v = params.get("crossdeck_internal");
|
|
2433
|
+
if (v === "1" || v === "true") {
|
|
2434
|
+
store.setItem(STORAGE_KEY, "1");
|
|
2435
|
+
} else if (v === "0" || v === "false") {
|
|
2436
|
+
store.removeItem(STORAGE_KEY);
|
|
2437
|
+
}
|
|
2438
|
+
} catch {
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
function isInternalOptOut() {
|
|
2442
|
+
try {
|
|
2443
|
+
return localStore()?.getItem(STORAGE_KEY) === "1";
|
|
2444
|
+
} catch {
|
|
2445
|
+
return false;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2332
2449
|
// src/web-vitals.ts
|
|
2333
2450
|
var WebVitalsTracker = class {
|
|
2334
2451
|
constructor(cfg, report) {
|
|
@@ -2837,6 +2954,13 @@ var CROSSDECK_ERROR_CODES = Object.freeze([
|
|
|
2837
2954
|
description: "A field is present but the value failed validation (wrong shape, wrong length, wrong enum value).",
|
|
2838
2955
|
resolution: "Read error.message for the specific field + reason. SDK-managed call sites should never emit this \u2014 file a bug if you do.",
|
|
2839
2956
|
retryable: false
|
|
2957
|
+
},
|
|
2958
|
+
{
|
|
2959
|
+
code: "sdk_version_unsupported",
|
|
2960
|
+
type: "version_error",
|
|
2961
|
+
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.",
|
|
2962
|
+
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/.",
|
|
2963
|
+
retryable: false
|
|
2840
2964
|
}
|
|
2841
2965
|
]);
|
|
2842
2966
|
function getErrorCode(code) {
|
|
@@ -2859,15 +2983,6 @@ var VerifierReporter = class {
|
|
|
2859
2983
|
constructor(ctx) {
|
|
2860
2984
|
this.ctx = ctx;
|
|
2861
2985
|
this.reentrancyDepth = 0;
|
|
2862
|
-
/** The live verifier set — attached by the SDK right after init so the
|
|
2863
|
-
* emit path can schema-lock its OWN outgoing telemetry against the real
|
|
2864
|
-
* payload. Null until attached (boot-time reports still work without it). */
|
|
2865
|
-
this.verifiers = null;
|
|
2866
|
-
}
|
|
2867
|
-
/** Hand the reporter the live verifier set so `reportFail` can run the
|
|
2868
|
-
* `onReportContractFailure` hook on the exact payload it's about to emit. */
|
|
2869
|
-
attachVerifiers(verifiers) {
|
|
2870
|
-
this.verifiers = verifiers;
|
|
2871
2986
|
}
|
|
2872
2987
|
/**
|
|
2873
2988
|
* Report a verifier result. Pass `operation` for hot-path results
|
|
@@ -2896,7 +3011,7 @@ var VerifierReporter = class {
|
|
|
2896
3011
|
if (this.reentrancyDepth > 0) return;
|
|
2897
3012
|
this.reentrancyDepth += 1;
|
|
2898
3013
|
try {
|
|
2899
|
-
|
|
3014
|
+
this.ctx.emitTelemetry({
|
|
2900
3015
|
contract_id: result.contractId,
|
|
2901
3016
|
sdk_version: this.ctx.sdkVersion,
|
|
2902
3017
|
sdk_platform: "web",
|
|
@@ -2904,13 +3019,7 @@ var VerifierReporter = class {
|
|
|
2904
3019
|
run_context: this.ctx.runContext,
|
|
2905
3020
|
run_id: this.ctx.runId,
|
|
2906
3021
|
verification_phase: phase
|
|
2907
|
-
};
|
|
2908
|
-
if (this.verifiers) {
|
|
2909
|
-
runOnReportContractFailure(this.verifiers, this, this.ctx, {
|
|
2910
|
-
outgoingPayload: payload
|
|
2911
|
-
});
|
|
2912
|
-
}
|
|
2913
|
-
this.ctx.emitTelemetry(payload);
|
|
3022
|
+
});
|
|
2914
3023
|
} finally {
|
|
2915
3024
|
this.reentrancyDepth -= 1;
|
|
2916
3025
|
}
|
|
@@ -3200,31 +3309,9 @@ var VERIFIER_FLUSH_INTERVAL_PARITY = {
|
|
|
3200
3309
|
"eventFlushIntervalMs default = 2000ms (Web/Node/RN/Swift/Android parity)",
|
|
3201
3310
|
nowMs() - t0
|
|
3202
3311
|
);
|
|
3203
|
-
},
|
|
3204
|
-
// Hot-path hook — on every real track() we re-affirm the LIVE configured
|
|
3205
|
-
// flush interval (carried on the observation), so the parity guarantee is
|
|
3206
|
-
// verified against the running SDK in the customer flow, not just a
|
|
3207
|
-
// canonical constant at boot. It can't change per-event, but observing it
|
|
3208
|
-
// per-event is what makes the contract genuinely runtime-verified.
|
|
3209
|
-
hooks: {
|
|
3210
|
-
onTrack(obs) {
|
|
3211
|
-
const t0 = nowMs();
|
|
3212
|
-
const CANONICAL_DEFAULT_MS = 2e3;
|
|
3213
|
-
const ms = obs.flushIntervalMs;
|
|
3214
|
-
if (typeof ms !== "number" || ms < 100 || ms > 6e4) {
|
|
3215
|
-
return fail(
|
|
3216
|
-
"flush-interval-parity",
|
|
3217
|
-
`live eventFlushIntervalMs=${ms} outside reasonable bounds [100, 60000]`,
|
|
3218
|
-
nowMs() - t0
|
|
3219
|
-
);
|
|
3220
|
-
}
|
|
3221
|
-
return pass(
|
|
3222
|
-
"flush-interval-parity",
|
|
3223
|
-
ms === CANONICAL_DEFAULT_MS ? "live eventFlushIntervalMs = 2000ms (canonical parity value)" : `live eventFlushIntervalMs = ${ms}ms (permitted override; canonical default 2000ms)`,
|
|
3224
|
-
nowMs() - t0
|
|
3225
|
-
);
|
|
3226
|
-
}
|
|
3227
3312
|
}
|
|
3313
|
+
// No hot-path hook — the flush interval is set once at start() and
|
|
3314
|
+
// never changes per-operation.
|
|
3228
3315
|
};
|
|
3229
3316
|
function buildFlushIntervalVerifier(configuredIntervalMs) {
|
|
3230
3317
|
return {
|
|
@@ -3351,26 +3438,6 @@ var CONTRACT_FAILED_FORBIDDEN_FIELDS = [
|
|
|
3351
3438
|
"referrer",
|
|
3352
3439
|
"deviceId"
|
|
3353
3440
|
];
|
|
3354
|
-
function contractFailedSchemaViolation(keys) {
|
|
3355
|
-
const allowed = /* @__PURE__ */ new Set([
|
|
3356
|
-
...CONTRACT_FAILED_REQUIRED_FIELDS,
|
|
3357
|
-
...CONTRACT_FAILED_OPTIONAL_FIELDS
|
|
3358
|
-
]);
|
|
3359
|
-
const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
|
|
3360
|
-
for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
|
|
3361
|
-
if (!keys.includes(required)) return `missing required field: ${required}`;
|
|
3362
|
-
}
|
|
3363
|
-
for (const k of keys) {
|
|
3364
|
-
if (forbidden.has(k)) return `forbidden field on wire: ${k}`;
|
|
3365
|
-
}
|
|
3366
|
-
for (const k of keys) {
|
|
3367
|
-
if (!allowed.has(k)) {
|
|
3368
|
-
return `unrecognised field on wire: ${k} (not in required \u222A optional)`;
|
|
3369
|
-
}
|
|
3370
|
-
}
|
|
3371
|
-
return null;
|
|
3372
|
-
}
|
|
3373
|
-
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`;
|
|
3374
3441
|
var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
|
|
3375
3442
|
contractId: "contract-failed-payload-schema-lock",
|
|
3376
3443
|
bootTest() {
|
|
@@ -3385,35 +3452,43 @@ var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
|
|
|
3385
3452
|
verification_phase: "boot"
|
|
3386
3453
|
};
|
|
3387
3454
|
const keys = Object.keys(syntheticPayload);
|
|
3388
|
-
const
|
|
3389
|
-
|
|
3390
|
-
|
|
3455
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
3456
|
+
...CONTRACT_FAILED_REQUIRED_FIELDS,
|
|
3457
|
+
...CONTRACT_FAILED_OPTIONAL_FIELDS
|
|
3458
|
+
]);
|
|
3459
|
+
const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
|
|
3460
|
+
for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
|
|
3461
|
+
if (!keys.includes(required)) {
|
|
3462
|
+
return fail(
|
|
3463
|
+
"contract-failed-payload-schema-lock",
|
|
3464
|
+
`missing required field: ${required}`,
|
|
3465
|
+
nowMs() - t0
|
|
3466
|
+
);
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
for (const k of keys) {
|
|
3470
|
+
if (forbidden.has(k)) {
|
|
3471
|
+
return fail(
|
|
3472
|
+
"contract-failed-payload-schema-lock",
|
|
3473
|
+
`forbidden field on wire: ${k}`,
|
|
3474
|
+
nowMs() - t0
|
|
3475
|
+
);
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
for (const k of keys) {
|
|
3479
|
+
if (!allowed.has(k)) {
|
|
3480
|
+
return fail(
|
|
3481
|
+
"contract-failed-payload-schema-lock",
|
|
3482
|
+
`unrecognised field on wire: ${k} (not in required \u222A optional)`,
|
|
3483
|
+
nowMs() - t0
|
|
3484
|
+
);
|
|
3485
|
+
}
|
|
3391
3486
|
}
|
|
3392
3487
|
return pass(
|
|
3393
3488
|
"contract-failed-payload-schema-lock",
|
|
3394
|
-
`${keys.length} ${
|
|
3489
|
+
`${keys.length} fields \u2286 required(${CONTRACT_FAILED_REQUIRED_FIELDS.length}) \u222A optional(${CONTRACT_FAILED_OPTIONAL_FIELDS.length}); ${CONTRACT_FAILED_FORBIDDEN_FIELDS.length} forbidden absent`,
|
|
3395
3490
|
nowMs() - t0
|
|
3396
3491
|
);
|
|
3397
|
-
},
|
|
3398
|
-
// Hot-path hook — fires on the REAL reportFail emit (VerifierReporter,
|
|
3399
|
-
// inside the re-entrancy guard) against the exact payload about to go on
|
|
3400
|
-
// the wire. This is the assertion the guard at reportFail was built in
|
|
3401
|
-
// anticipation of: every reliability-channel write is schema-locked in the
|
|
3402
|
-
// field, against real data, not just the synthetic boot mirror.
|
|
3403
|
-
hooks: {
|
|
3404
|
-
onReportContractFailure(obs) {
|
|
3405
|
-
const t0 = nowMs();
|
|
3406
|
-
const keys = Object.keys(obs.outgoingPayload);
|
|
3407
|
-
const violation = contractFailedSchemaViolation(keys);
|
|
3408
|
-
if (violation) {
|
|
3409
|
-
return fail("contract-failed-payload-schema-lock", violation, nowMs() - t0);
|
|
3410
|
-
}
|
|
3411
|
-
return pass(
|
|
3412
|
-
"contract-failed-payload-schema-lock",
|
|
3413
|
-
`${keys.length} ${CONTRACT_FAILED_OK_EVIDENCE}`,
|
|
3414
|
-
nowMs() - t0
|
|
3415
|
-
);
|
|
3416
|
-
}
|
|
3417
3492
|
}
|
|
3418
3493
|
};
|
|
3419
3494
|
var BACKEND_WIRE_CODES = Object.freeze([
|
|
@@ -3431,7 +3506,8 @@ var BACKEND_WIRE_CODES = Object.freeze([
|
|
|
3431
3506
|
"google_not_supported",
|
|
3432
3507
|
"stripe_not_supported",
|
|
3433
3508
|
"missing_required_param",
|
|
3434
|
-
"invalid_param_value"
|
|
3509
|
+
"invalid_param_value",
|
|
3510
|
+
"sdk_version_unsupported"
|
|
3435
3511
|
]);
|
|
3436
3512
|
var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
|
|
3437
3513
|
contractId: "sdk-error-codes-catalogue",
|
|
@@ -3464,39 +3540,6 @@ var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
|
|
|
3464
3540
|
nowMs() - t0
|
|
3465
3541
|
);
|
|
3466
3542
|
}
|
|
3467
|
-
},
|
|
3468
|
-
// Hot-path hook — when the SDK parses a real wire error, assert the code
|
|
3469
|
-
// the customer actually hit carries usable remediation in the shipped
|
|
3470
|
-
// catalogue. Scoped to BACKEND_WIRE_CODES (the contract's set); other
|
|
3471
|
-
// codes are out of scope and pass. This turns the catalogue from a
|
|
3472
|
-
// boot-only completeness claim into a per-error field assertion: a backend
|
|
3473
|
-
// code that ships without remediation is caught the first time a customer
|
|
3474
|
-
// hits it, not only if the off-by-default boot self-test ran.
|
|
3475
|
-
hooks: {
|
|
3476
|
-
onErrorParse(obs) {
|
|
3477
|
-
const t0 = nowMs();
|
|
3478
|
-
const code = obs.errorCode;
|
|
3479
|
-
if (!code || !BACKEND_WIRE_CODES.includes(code)) {
|
|
3480
|
-
return pass(
|
|
3481
|
-
"sdk-error-codes-catalogue",
|
|
3482
|
-
`wire code "${code || "(none)"}" is out of the backend-catalogue scope`,
|
|
3483
|
-
nowMs() - t0
|
|
3484
|
-
);
|
|
3485
|
-
}
|
|
3486
|
-
const entry = getErrorCode(code);
|
|
3487
|
-
if (!entry || !entry.description || entry.description.trim().length === 0 || !entry.resolution || entry.resolution.trim().length === 0) {
|
|
3488
|
-
return fail(
|
|
3489
|
-
"sdk-error-codes-catalogue",
|
|
3490
|
-
`backend wire code "${code}" hit in the field has no description+resolution in the shipped catalogue`,
|
|
3491
|
-
nowMs() - t0
|
|
3492
|
-
);
|
|
3493
|
-
}
|
|
3494
|
-
return pass(
|
|
3495
|
-
"sdk-error-codes-catalogue",
|
|
3496
|
-
`backend wire code "${code}" carries description + resolution`,
|
|
3497
|
-
nowMs() - t0
|
|
3498
|
-
);
|
|
3499
|
-
}
|
|
3500
3543
|
}
|
|
3501
3544
|
};
|
|
3502
3545
|
var STATIC_VERIFIERS = Object.freeze([
|
|
@@ -3620,24 +3663,6 @@ function runOnErrorParse(verifiers, reporter, ctx, obs) {
|
|
|
3620
3663
|
reporter.report(result, "hot_path", "errorParse");
|
|
3621
3664
|
}
|
|
3622
3665
|
}
|
|
3623
|
-
function runOnReportContractFailure(verifiers, reporter, ctx, obs) {
|
|
3624
|
-
if (ctx.disableContractAssertions) return;
|
|
3625
|
-
for (const verifier of verifiers) {
|
|
3626
|
-
const hook = verifier.hooks?.onReportContractFailure;
|
|
3627
|
-
if (!hook) continue;
|
|
3628
|
-
let result;
|
|
3629
|
-
try {
|
|
3630
|
-
result = hook(obs);
|
|
3631
|
-
} catch (err) {
|
|
3632
|
-
result = fail(
|
|
3633
|
-
verifier.contractId,
|
|
3634
|
-
`hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
|
|
3635
|
-
0
|
|
3636
|
-
);
|
|
3637
|
-
}
|
|
3638
|
-
reporter.report(result, "hot_path", "reportContractFailure");
|
|
3639
|
-
}
|
|
3640
|
-
}
|
|
3641
3666
|
function defaultDebugModeFlag() {
|
|
3642
3667
|
try {
|
|
3643
3668
|
if (typeof process !== "undefined" && process.env) {
|
|
@@ -3789,18 +3814,18 @@ function isInAppFrame(filename) {
|
|
|
3789
3814
|
if (/\/crossdeck\.umd\.min\.js$/.test(filename)) return false;
|
|
3790
3815
|
return true;
|
|
3791
3816
|
}
|
|
3792
|
-
function fingerprintError(message, frames,
|
|
3817
|
+
function fingerprintError(message, frames, location2) {
|
|
3793
3818
|
const inAppFrames = frames.filter((f) => f.in_app).slice(0, 3);
|
|
3794
3819
|
const parts = [
|
|
3795
3820
|
(message || "").slice(0, 200),
|
|
3796
3821
|
...inAppFrames.map((f) => `${f.function}@${f.filename}:${f.lineno}`)
|
|
3797
3822
|
];
|
|
3798
|
-
if (inAppFrames.length === 0 &&
|
|
3823
|
+
if (inAppFrames.length === 0 && location2) {
|
|
3799
3824
|
const loc = [
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3825
|
+
location2.errorType ?? "",
|
|
3826
|
+
location2.filename ?? "",
|
|
3827
|
+
location2.lineno ?? "",
|
|
3828
|
+
location2.colno ?? ""
|
|
3804
3829
|
].join(":");
|
|
3805
3830
|
if (loc !== ":::") parts.push(loc);
|
|
3806
3831
|
}
|
|
@@ -4470,9 +4495,6 @@ var CrossdeckClient = class {
|
|
|
4470
4495
|
this.verifiers = null;
|
|
4471
4496
|
this.verifierReporter = null;
|
|
4472
4497
|
this.verifierCtx = null;
|
|
4473
|
-
// The live configured event-flush interval, surfaced to the track-path
|
|
4474
|
-
// verifier so flush-interval-parity validates the real cadence per event.
|
|
4475
|
-
this.flushIntervalMs = 2e3;
|
|
4476
4498
|
}
|
|
4477
4499
|
/**
|
|
4478
4500
|
* Boot the SDK. Idempotent — calling init twice with the same options
|
|
@@ -4642,6 +4664,13 @@ var CrossdeckClient = class {
|
|
|
4642
4664
|
headline,
|
|
4643
4665
|
{ ...info }
|
|
4644
4666
|
);
|
|
4667
|
+
},
|
|
4668
|
+
onParked: (info) => {
|
|
4669
|
+
debug.emit(
|
|
4670
|
+
"sdk.parked",
|
|
4671
|
+
`[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.`,
|
|
4672
|
+
{ ...info }
|
|
4673
|
+
);
|
|
4645
4674
|
}
|
|
4646
4675
|
});
|
|
4647
4676
|
const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
|
|
@@ -4649,6 +4678,7 @@ var CrossdeckClient = class {
|
|
|
4649
4678
|
persistIdentity ? effectiveStorage : new MemoryStorage(),
|
|
4650
4679
|
opts.storagePrefix
|
|
4651
4680
|
);
|
|
4681
|
+
processInternalOptOutUrl();
|
|
4652
4682
|
const consent = new ConsentManager({ respectDnt: options.respectDnt === true });
|
|
4653
4683
|
if (consent.isDntDenied) {
|
|
4654
4684
|
debug.emit(
|
|
@@ -4752,8 +4782,6 @@ var CrossdeckClient = class {
|
|
|
4752
4782
|
this.verifierReporter = new VerifierReporter(this.verifierCtx);
|
|
4753
4783
|
const flushVerifier = buildFlushIntervalVerifier(opts.eventFlushIntervalMs);
|
|
4754
4784
|
this.verifiers = Object.freeze([...STATIC_VERIFIERS, flushVerifier]);
|
|
4755
|
-
this.flushIntervalMs = opts.eventFlushIntervalMs;
|
|
4756
|
-
this.verifierReporter.attachVerifiers(this.verifiers);
|
|
4757
4785
|
if (localDevMode) {
|
|
4758
4786
|
const verifyAtBootLocal = options.verifyContractsAtBoot ?? debugDefault;
|
|
4759
4787
|
if (verifyAtBootLocal && this.verifierReporter) {
|
|
@@ -5304,8 +5332,27 @@ var CrossdeckClient = class {
|
|
|
5304
5332
|
);
|
|
5305
5333
|
}
|
|
5306
5334
|
}
|
|
5335
|
+
const seq = s.autoTracker?.nextSeq() ?? 0;
|
|
5336
|
+
const occurrenceTimestamp = Date.now();
|
|
5307
5337
|
s.autoTracker?.markActivity();
|
|
5308
|
-
const
|
|
5338
|
+
const wireContext = {};
|
|
5339
|
+
const di = s.deviceInfo;
|
|
5340
|
+
if (di.os) wireContext.os = di.os;
|
|
5341
|
+
if (di.osVersion) wireContext.osVersion = di.osVersion;
|
|
5342
|
+
const appVer = s.options.appVersion;
|
|
5343
|
+
if (appVer) wireContext.appVersion = appVer;
|
|
5344
|
+
wireContext.sdkName = SDK_NAME;
|
|
5345
|
+
wireContext.sdkVersion = s.options.sdkVersion;
|
|
5346
|
+
if (di.locale) wireContext.locale = di.locale;
|
|
5347
|
+
if (di.timezone) wireContext.timezone = di.timezone;
|
|
5348
|
+
if (di.browser) wireContext.browser = di.browser;
|
|
5349
|
+
if (di.browserVersion) wireContext.browserVersion = di.browserVersion;
|
|
5350
|
+
const enriched = {};
|
|
5351
|
+
if (di.screenWidth !== void 0) enriched.screenWidth = di.screenWidth;
|
|
5352
|
+
if (di.screenHeight !== void 0) enriched.screenHeight = di.screenHeight;
|
|
5353
|
+
if (di.viewportWidth !== void 0) enriched.viewportWidth = di.viewportWidth;
|
|
5354
|
+
if (di.viewportHeight !== void 0) enriched.viewportHeight = di.viewportHeight;
|
|
5355
|
+
if (di.devicePixelRatio !== void 0) enriched.devicePixelRatio = di.devicePixelRatio;
|
|
5309
5356
|
const sessionId = s.autoTracker?.currentSessionId;
|
|
5310
5357
|
if (sessionId) enriched.sessionId = sessionId;
|
|
5311
5358
|
const pageviewId = s.autoTracker?.currentPageviewId;
|
|
@@ -5328,17 +5375,24 @@ var CrossdeckClient = class {
|
|
|
5328
5375
|
}
|
|
5329
5376
|
}
|
|
5330
5377
|
const supers = s.superProps.getSuperProperties();
|
|
5331
|
-
Object.
|
|
5378
|
+
for (const k of Object.keys(supers)) {
|
|
5379
|
+
if (!(k in enriched)) enriched[k] = supers[k];
|
|
5380
|
+
}
|
|
5332
5381
|
const groupIds = s.superProps.getGroupIds();
|
|
5333
5382
|
if (Object.keys(groupIds).length > 0) {
|
|
5334
5383
|
enriched.$groups = groupIds;
|
|
5335
5384
|
}
|
|
5336
5385
|
Object.assign(enriched, validation.properties);
|
|
5386
|
+
if (isInternalOptOut()) {
|
|
5387
|
+
enriched[INTERNAL_OPT_OUT_PROPERTY] = true;
|
|
5388
|
+
}
|
|
5337
5389
|
const finalProperties = s.scrubPii ? scrubPiiFromProperties(enriched) : enriched;
|
|
5338
5390
|
const event = {
|
|
5339
5391
|
eventId: this.mintEventId(),
|
|
5340
5392
|
name,
|
|
5341
|
-
timestamp:
|
|
5393
|
+
timestamp: occurrenceTimestamp,
|
|
5394
|
+
seq,
|
|
5395
|
+
context: wireContext,
|
|
5342
5396
|
properties: finalProperties
|
|
5343
5397
|
};
|
|
5344
5398
|
Object.assign(event, this.identityHintForEvent());
|
|
@@ -5348,8 +5402,7 @@ var CrossdeckClient = class {
|
|
|
5348
5402
|
callerProperties: validation.properties,
|
|
5349
5403
|
superProperties: supers,
|
|
5350
5404
|
deviceProperties: s.deviceInfo,
|
|
5351
|
-
mergedProperties: enriched
|
|
5352
|
-
flushIntervalMs: this.flushIntervalMs
|
|
5405
|
+
mergedProperties: enriched
|
|
5353
5406
|
});
|
|
5354
5407
|
}
|
|
5355
5408
|
if (!isError && !isWebVital) {
|