@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.mjs
CHANGED
|
@@ -11,6 +11,8 @@ var CrossdeckError = class _CrossdeckError extends Error {
|
|
|
11
11
|
this.requestId = payload.requestId;
|
|
12
12
|
this.status = payload.status;
|
|
13
13
|
this.retryAfterMs = payload.retryAfterMs;
|
|
14
|
+
this.minVersion = payload.minVersion;
|
|
15
|
+
this.surface = payload.surface;
|
|
14
16
|
Object.setPrototypeOf(this, _CrossdeckError.prototype);
|
|
15
17
|
}
|
|
16
18
|
};
|
|
@@ -31,7 +33,10 @@ async function crossdeckErrorFromResponse(res) {
|
|
|
31
33
|
message: envelope.message ?? `HTTP ${res.status}`,
|
|
32
34
|
requestId: envelope.request_id ?? requestId,
|
|
33
35
|
status: res.status,
|
|
34
|
-
retryAfterMs
|
|
36
|
+
retryAfterMs,
|
|
37
|
+
// PARK metadata, present only on a 426 / sdk_version_unsupported body.
|
|
38
|
+
minVersion: typeof envelope.minVersion === "string" ? envelope.minVersion : void 0,
|
|
39
|
+
surface: typeof envelope.surface === "string" ? envelope.surface : void 0
|
|
35
40
|
});
|
|
36
41
|
}
|
|
37
42
|
return new CrossdeckError({
|
|
@@ -67,7 +72,7 @@ function typeMapForStatus(status) {
|
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
// src/_version.ts
|
|
70
|
-
var SDK_VERSION = "1.
|
|
75
|
+
var SDK_VERSION = "1.8.0";
|
|
71
76
|
var SDK_NAME = "@cross-deck/web";
|
|
72
77
|
|
|
73
78
|
// src/http.ts
|
|
@@ -907,6 +912,18 @@ var EventQueue = class {
|
|
|
907
912
|
this.cancelTimer = null;
|
|
908
913
|
this.firstFlushFired = false;
|
|
909
914
|
this.nextRetryAt = null;
|
|
915
|
+
/**
|
|
916
|
+
* PARK state (HTTP 426 / `sdk_version_unsupported`). Once parked, the
|
|
917
|
+
* queue stops flushing — the server has told us our wire dialect is too
|
|
918
|
+
* old, and retrying the same payload only burns the user's battery and
|
|
919
|
+
* bandwidth and drips pointless rejects into the server logs until the
|
|
920
|
+
* app ships an upgraded SDK. The held events stay durable (persisted)
|
|
921
|
+
* and are delivered by the next boot's rehydrate, post-upgrade. Parked
|
|
922
|
+
* is per-instance: a fresh boot (the upgraded build) starts unparked.
|
|
923
|
+
*/
|
|
924
|
+
this.parked = false;
|
|
925
|
+
/** One developer-facing console warning per instance — never per-event spam. */
|
|
926
|
+
this.parkWarned = false;
|
|
910
927
|
this.retry = new RetryPolicy(cfg.retry ?? {});
|
|
911
928
|
this.persistent = cfg.persistentStore ?? null;
|
|
912
929
|
if (this.persistent) {
|
|
@@ -962,6 +979,7 @@ var EventQueue = class {
|
|
|
962
979
|
* flushes (pagehide / visibilitychange→hidden / beforeunload).
|
|
963
980
|
*/
|
|
964
981
|
async flush(options = {}) {
|
|
982
|
+
if (this.parked) return null;
|
|
965
983
|
let batch;
|
|
966
984
|
let batchId;
|
|
967
985
|
if (this.pendingBatch !== null && this.pendingBatchId !== null) {
|
|
@@ -983,9 +1001,13 @@ var EventQueue = class {
|
|
|
983
1001
|
const env = this.cfg.envelope();
|
|
984
1002
|
const result = await this.cfg.http.request("POST", "/events", {
|
|
985
1003
|
body: {
|
|
986
|
-
//
|
|
987
|
-
//
|
|
988
|
-
//
|
|
1004
|
+
// Event Envelope v1 batch envelope (backend/docs/
|
|
1005
|
+
// event-envelope-spec-v1.md §1). `envelopeVersion` is the
|
|
1006
|
+
// schema/wire version the server parses against ("can I parse
|
|
1007
|
+
// this?") and is DISTINCT from `sdk.version` ("which build is in
|
|
1008
|
+
// the wild?") — two questions, two fields, never conflated. The
|
|
1009
|
+
// backend refuses payloads with a missing/unknown envelopeVersion.
|
|
1010
|
+
envelopeVersion: 1,
|
|
989
1011
|
appId: env.appId,
|
|
990
1012
|
environment: env.environment,
|
|
991
1013
|
sdk: env.sdk,
|
|
@@ -1009,6 +1031,29 @@ var EventQueue = class {
|
|
|
1009
1031
|
} catch (err) {
|
|
1010
1032
|
const message = err instanceof Error ? err.message : String(err);
|
|
1011
1033
|
this.lastError = message;
|
|
1034
|
+
if (isVersionRejected(err)) {
|
|
1035
|
+
this.parked = true;
|
|
1036
|
+
this.buffer = [...batch, ...this.buffer];
|
|
1037
|
+
if (this.buffer.length > HARD_BUFFER_CAP) {
|
|
1038
|
+
const overflow = this.buffer.length - HARD_BUFFER_CAP;
|
|
1039
|
+
this.buffer.splice(0, overflow);
|
|
1040
|
+
this.dropped += overflow;
|
|
1041
|
+
}
|
|
1042
|
+
this.pendingBatch = null;
|
|
1043
|
+
this.pendingBatchId = null;
|
|
1044
|
+
this.inFlight -= batch.length;
|
|
1045
|
+
this.persistAll();
|
|
1046
|
+
this.cfg.onBufferChange?.(this.buffer.length);
|
|
1047
|
+
const minVersion = versionRejectionFloor(err);
|
|
1048
|
+
if (!this.parkWarned) {
|
|
1049
|
+
this.parkWarned = true;
|
|
1050
|
+
console.warn(
|
|
1051
|
+
`[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.`
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
this.cfg.onParked?.({ minVersion, surface: versionRejectionSurface(err) });
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
1012
1057
|
if (isPermanent4xx(err)) {
|
|
1013
1058
|
const droppedCount = batch.length;
|
|
1014
1059
|
this.pendingBatch = null;
|
|
@@ -1127,8 +1172,22 @@ function isPermanent4xx(err) {
|
|
|
1127
1172
|
if (typeof status !== "number" || !Number.isFinite(status)) return false;
|
|
1128
1173
|
if (status < 400 || status >= 500) return false;
|
|
1129
1174
|
if (status === 408 || status === 429) return false;
|
|
1175
|
+
if (status === 426) return false;
|
|
1130
1176
|
return true;
|
|
1131
1177
|
}
|
|
1178
|
+
function isVersionRejected(err) {
|
|
1179
|
+
if (!err || typeof err !== "object") return false;
|
|
1180
|
+
if (err.status === 426) return true;
|
|
1181
|
+
return err.code === "sdk_version_unsupported";
|
|
1182
|
+
}
|
|
1183
|
+
function versionRejectionFloor(err) {
|
|
1184
|
+
const v = err?.minVersion;
|
|
1185
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1186
|
+
}
|
|
1187
|
+
function versionRejectionSurface(err) {
|
|
1188
|
+
const v = err?.surface;
|
|
1189
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1190
|
+
}
|
|
1132
1191
|
function defaultScheduler(fn, ms) {
|
|
1133
1192
|
const id = setTimeout(fn, ms);
|
|
1134
1193
|
if (typeof id.unref === "function") {
|
|
@@ -1440,6 +1499,15 @@ var AutoTracker = class {
|
|
|
1440
1499
|
this.cleanups = [];
|
|
1441
1500
|
/** Last time we flushed lastActivityAt to storage (throttle gate). */
|
|
1442
1501
|
this.lastPersistAt = 0;
|
|
1502
|
+
/**
|
|
1503
|
+
* Event Envelope v1 §3 — per-session monotonic sequence counter.
|
|
1504
|
+
* The single owner of the seq counter lives here, alongside all other
|
|
1505
|
+
* session state. Incremented atomically at track() time (via nextSeq()),
|
|
1506
|
+
* reset to 0 whenever a new session boundary is crossed (startNewSession
|
|
1507
|
+
* path + resetSession). Persists across background/foreground within a
|
|
1508
|
+
* session — only a real session boundary (not a tab hide/show) resets it.
|
|
1509
|
+
*/
|
|
1510
|
+
this._sessionSeq = 0;
|
|
1443
1511
|
/**
|
|
1444
1512
|
* Stable per-page-view identifier. Minted at every `page.viewed`
|
|
1445
1513
|
* emission and attached to every subsequent event until the next
|
|
@@ -1524,6 +1592,20 @@ var AutoTracker = class {
|
|
|
1524
1592
|
get currentAcquisition() {
|
|
1525
1593
|
return this.session?.acquisition ?? EMPTY_ACQUISITION;
|
|
1526
1594
|
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Event Envelope v1 §3 — return the next seq value for the current session
|
|
1597
|
+
* and advance the counter. Called SYNCHRONOUSLY at track() time, before any
|
|
1598
|
+
* async dispatch, so the seq assignment order is deterministic (call order,
|
|
1599
|
+
* not scheduler luck). The counter is reset to 0 at every session boundary;
|
|
1600
|
+
* it is NOT reset by tab hide/show (spec §3 clause 1: backgrounding does not
|
|
1601
|
+
* reset seq). When no session is active (Node, before init, after uninstall),
|
|
1602
|
+
* returns 0 and leaves the counter unchanged — fallback, not an error.
|
|
1603
|
+
*/
|
|
1604
|
+
nextSeq() {
|
|
1605
|
+
const seq = this._sessionSeq;
|
|
1606
|
+
this._sessionSeq += 1;
|
|
1607
|
+
return seq;
|
|
1608
|
+
}
|
|
1527
1609
|
// ---------- sessions ----------
|
|
1528
1610
|
installSessionTracking() {
|
|
1529
1611
|
const now = Date.now();
|
|
@@ -1575,6 +1657,7 @@ var AutoTracker = class {
|
|
|
1575
1657
|
}
|
|
1576
1658
|
startNewSession() {
|
|
1577
1659
|
const now = Date.now();
|
|
1660
|
+
this._sessionSeq = 0;
|
|
1578
1661
|
return {
|
|
1579
1662
|
sessionId: mintSessionId(),
|
|
1580
1663
|
startedAt: now,
|
|
@@ -2303,6 +2386,40 @@ function writeJson(storage, key, value) {
|
|
|
2303
2386
|
}
|
|
2304
2387
|
}
|
|
2305
2388
|
|
|
2389
|
+
// src/internal-opt-out.ts
|
|
2390
|
+
var INTERNAL_OPT_OUT_PROPERTY = "$crossdeck_internal";
|
|
2391
|
+
var STORAGE_KEY = "crossdeck.internalOptOut";
|
|
2392
|
+
function localStore() {
|
|
2393
|
+
try {
|
|
2394
|
+
return typeof localStorage !== "undefined" ? localStorage : null;
|
|
2395
|
+
} catch {
|
|
2396
|
+
return null;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
function processInternalOptOutUrl() {
|
|
2400
|
+
try {
|
|
2401
|
+
const search = typeof location !== "undefined" ? location.search : "";
|
|
2402
|
+
const params = new URLSearchParams(search || "");
|
|
2403
|
+
if (!params.has("crossdeck_internal")) return;
|
|
2404
|
+
const store = localStore();
|
|
2405
|
+
if (!store) return;
|
|
2406
|
+
const v = params.get("crossdeck_internal");
|
|
2407
|
+
if (v === "1" || v === "true") {
|
|
2408
|
+
store.setItem(STORAGE_KEY, "1");
|
|
2409
|
+
} else if (v === "0" || v === "false") {
|
|
2410
|
+
store.removeItem(STORAGE_KEY);
|
|
2411
|
+
}
|
|
2412
|
+
} catch {
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
function isInternalOptOut() {
|
|
2416
|
+
try {
|
|
2417
|
+
return localStore()?.getItem(STORAGE_KEY) === "1";
|
|
2418
|
+
} catch {
|
|
2419
|
+
return false;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2306
2423
|
// src/web-vitals.ts
|
|
2307
2424
|
var WebVitalsTracker = class {
|
|
2308
2425
|
constructor(cfg, report) {
|
|
@@ -2811,6 +2928,13 @@ var CROSSDECK_ERROR_CODES = Object.freeze([
|
|
|
2811
2928
|
description: "A field is present but the value failed validation (wrong shape, wrong length, wrong enum value).",
|
|
2812
2929
|
resolution: "Read error.message for the specific field + reason. SDK-managed call sites should never emit this \u2014 file a bug if you do.",
|
|
2813
2930
|
retryable: false
|
|
2931
|
+
},
|
|
2932
|
+
{
|
|
2933
|
+
code: "sdk_version_unsupported",
|
|
2934
|
+
type: "version_error",
|
|
2935
|
+
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.",
|
|
2936
|
+
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/.",
|
|
2937
|
+
retryable: false
|
|
2814
2938
|
}
|
|
2815
2939
|
]);
|
|
2816
2940
|
function getErrorCode(code) {
|
|
@@ -2833,15 +2957,6 @@ var VerifierReporter = class {
|
|
|
2833
2957
|
constructor(ctx) {
|
|
2834
2958
|
this.ctx = ctx;
|
|
2835
2959
|
this.reentrancyDepth = 0;
|
|
2836
|
-
/** The live verifier set — attached by the SDK right after init so the
|
|
2837
|
-
* emit path can schema-lock its OWN outgoing telemetry against the real
|
|
2838
|
-
* payload. Null until attached (boot-time reports still work without it). */
|
|
2839
|
-
this.verifiers = null;
|
|
2840
|
-
}
|
|
2841
|
-
/** Hand the reporter the live verifier set so `reportFail` can run the
|
|
2842
|
-
* `onReportContractFailure` hook on the exact payload it's about to emit. */
|
|
2843
|
-
attachVerifiers(verifiers) {
|
|
2844
|
-
this.verifiers = verifiers;
|
|
2845
2960
|
}
|
|
2846
2961
|
/**
|
|
2847
2962
|
* Report a verifier result. Pass `operation` for hot-path results
|
|
@@ -2870,7 +2985,7 @@ var VerifierReporter = class {
|
|
|
2870
2985
|
if (this.reentrancyDepth > 0) return;
|
|
2871
2986
|
this.reentrancyDepth += 1;
|
|
2872
2987
|
try {
|
|
2873
|
-
|
|
2988
|
+
this.ctx.emitTelemetry({
|
|
2874
2989
|
contract_id: result.contractId,
|
|
2875
2990
|
sdk_version: this.ctx.sdkVersion,
|
|
2876
2991
|
sdk_platform: "web",
|
|
@@ -2878,13 +2993,7 @@ var VerifierReporter = class {
|
|
|
2878
2993
|
run_context: this.ctx.runContext,
|
|
2879
2994
|
run_id: this.ctx.runId,
|
|
2880
2995
|
verification_phase: phase
|
|
2881
|
-
};
|
|
2882
|
-
if (this.verifiers) {
|
|
2883
|
-
runOnReportContractFailure(this.verifiers, this, this.ctx, {
|
|
2884
|
-
outgoingPayload: payload
|
|
2885
|
-
});
|
|
2886
|
-
}
|
|
2887
|
-
this.ctx.emitTelemetry(payload);
|
|
2996
|
+
});
|
|
2888
2997
|
} finally {
|
|
2889
2998
|
this.reentrancyDepth -= 1;
|
|
2890
2999
|
}
|
|
@@ -3174,31 +3283,9 @@ var VERIFIER_FLUSH_INTERVAL_PARITY = {
|
|
|
3174
3283
|
"eventFlushIntervalMs default = 2000ms (Web/Node/RN/Swift/Android parity)",
|
|
3175
3284
|
nowMs() - t0
|
|
3176
3285
|
);
|
|
3177
|
-
},
|
|
3178
|
-
// Hot-path hook — on every real track() we re-affirm the LIVE configured
|
|
3179
|
-
// flush interval (carried on the observation), so the parity guarantee is
|
|
3180
|
-
// verified against the running SDK in the customer flow, not just a
|
|
3181
|
-
// canonical constant at boot. It can't change per-event, but observing it
|
|
3182
|
-
// per-event is what makes the contract genuinely runtime-verified.
|
|
3183
|
-
hooks: {
|
|
3184
|
-
onTrack(obs) {
|
|
3185
|
-
const t0 = nowMs();
|
|
3186
|
-
const CANONICAL_DEFAULT_MS = 2e3;
|
|
3187
|
-
const ms = obs.flushIntervalMs;
|
|
3188
|
-
if (typeof ms !== "number" || ms < 100 || ms > 6e4) {
|
|
3189
|
-
return fail(
|
|
3190
|
-
"flush-interval-parity",
|
|
3191
|
-
`live eventFlushIntervalMs=${ms} outside reasonable bounds [100, 60000]`,
|
|
3192
|
-
nowMs() - t0
|
|
3193
|
-
);
|
|
3194
|
-
}
|
|
3195
|
-
return pass(
|
|
3196
|
-
"flush-interval-parity",
|
|
3197
|
-
ms === CANONICAL_DEFAULT_MS ? "live eventFlushIntervalMs = 2000ms (canonical parity value)" : `live eventFlushIntervalMs = ${ms}ms (permitted override; canonical default 2000ms)`,
|
|
3198
|
-
nowMs() - t0
|
|
3199
|
-
);
|
|
3200
|
-
}
|
|
3201
3286
|
}
|
|
3287
|
+
// No hot-path hook — the flush interval is set once at start() and
|
|
3288
|
+
// never changes per-operation.
|
|
3202
3289
|
};
|
|
3203
3290
|
function buildFlushIntervalVerifier(configuredIntervalMs) {
|
|
3204
3291
|
return {
|
|
@@ -3325,26 +3412,6 @@ var CONTRACT_FAILED_FORBIDDEN_FIELDS = [
|
|
|
3325
3412
|
"referrer",
|
|
3326
3413
|
"deviceId"
|
|
3327
3414
|
];
|
|
3328
|
-
function contractFailedSchemaViolation(keys) {
|
|
3329
|
-
const allowed = /* @__PURE__ */ new Set([
|
|
3330
|
-
...CONTRACT_FAILED_REQUIRED_FIELDS,
|
|
3331
|
-
...CONTRACT_FAILED_OPTIONAL_FIELDS
|
|
3332
|
-
]);
|
|
3333
|
-
const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
|
|
3334
|
-
for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
|
|
3335
|
-
if (!keys.includes(required)) return `missing required field: ${required}`;
|
|
3336
|
-
}
|
|
3337
|
-
for (const k of keys) {
|
|
3338
|
-
if (forbidden.has(k)) return `forbidden field on wire: ${k}`;
|
|
3339
|
-
}
|
|
3340
|
-
for (const k of keys) {
|
|
3341
|
-
if (!allowed.has(k)) {
|
|
3342
|
-
return `unrecognised field on wire: ${k} (not in required \u222A optional)`;
|
|
3343
|
-
}
|
|
3344
|
-
}
|
|
3345
|
-
return null;
|
|
3346
|
-
}
|
|
3347
|
-
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`;
|
|
3348
3415
|
var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
|
|
3349
3416
|
contractId: "contract-failed-payload-schema-lock",
|
|
3350
3417
|
bootTest() {
|
|
@@ -3359,35 +3426,43 @@ var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
|
|
|
3359
3426
|
verification_phase: "boot"
|
|
3360
3427
|
};
|
|
3361
3428
|
const keys = Object.keys(syntheticPayload);
|
|
3362
|
-
const
|
|
3363
|
-
|
|
3364
|
-
|
|
3429
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
3430
|
+
...CONTRACT_FAILED_REQUIRED_FIELDS,
|
|
3431
|
+
...CONTRACT_FAILED_OPTIONAL_FIELDS
|
|
3432
|
+
]);
|
|
3433
|
+
const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
|
|
3434
|
+
for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
|
|
3435
|
+
if (!keys.includes(required)) {
|
|
3436
|
+
return fail(
|
|
3437
|
+
"contract-failed-payload-schema-lock",
|
|
3438
|
+
`missing required field: ${required}`,
|
|
3439
|
+
nowMs() - t0
|
|
3440
|
+
);
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
for (const k of keys) {
|
|
3444
|
+
if (forbidden.has(k)) {
|
|
3445
|
+
return fail(
|
|
3446
|
+
"contract-failed-payload-schema-lock",
|
|
3447
|
+
`forbidden field on wire: ${k}`,
|
|
3448
|
+
nowMs() - t0
|
|
3449
|
+
);
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
for (const k of keys) {
|
|
3453
|
+
if (!allowed.has(k)) {
|
|
3454
|
+
return fail(
|
|
3455
|
+
"contract-failed-payload-schema-lock",
|
|
3456
|
+
`unrecognised field on wire: ${k} (not in required \u222A optional)`,
|
|
3457
|
+
nowMs() - t0
|
|
3458
|
+
);
|
|
3459
|
+
}
|
|
3365
3460
|
}
|
|
3366
3461
|
return pass(
|
|
3367
3462
|
"contract-failed-payload-schema-lock",
|
|
3368
|
-
`${keys.length} ${
|
|
3463
|
+
`${keys.length} fields \u2286 required(${CONTRACT_FAILED_REQUIRED_FIELDS.length}) \u222A optional(${CONTRACT_FAILED_OPTIONAL_FIELDS.length}); ${CONTRACT_FAILED_FORBIDDEN_FIELDS.length} forbidden absent`,
|
|
3369
3464
|
nowMs() - t0
|
|
3370
3465
|
);
|
|
3371
|
-
},
|
|
3372
|
-
// Hot-path hook — fires on the REAL reportFail emit (VerifierReporter,
|
|
3373
|
-
// inside the re-entrancy guard) against the exact payload about to go on
|
|
3374
|
-
// the wire. This is the assertion the guard at reportFail was built in
|
|
3375
|
-
// anticipation of: every reliability-channel write is schema-locked in the
|
|
3376
|
-
// field, against real data, not just the synthetic boot mirror.
|
|
3377
|
-
hooks: {
|
|
3378
|
-
onReportContractFailure(obs) {
|
|
3379
|
-
const t0 = nowMs();
|
|
3380
|
-
const keys = Object.keys(obs.outgoingPayload);
|
|
3381
|
-
const violation = contractFailedSchemaViolation(keys);
|
|
3382
|
-
if (violation) {
|
|
3383
|
-
return fail("contract-failed-payload-schema-lock", violation, nowMs() - t0);
|
|
3384
|
-
}
|
|
3385
|
-
return pass(
|
|
3386
|
-
"contract-failed-payload-schema-lock",
|
|
3387
|
-
`${keys.length} ${CONTRACT_FAILED_OK_EVIDENCE}`,
|
|
3388
|
-
nowMs() - t0
|
|
3389
|
-
);
|
|
3390
|
-
}
|
|
3391
3466
|
}
|
|
3392
3467
|
};
|
|
3393
3468
|
var BACKEND_WIRE_CODES = Object.freeze([
|
|
@@ -3405,7 +3480,8 @@ var BACKEND_WIRE_CODES = Object.freeze([
|
|
|
3405
3480
|
"google_not_supported",
|
|
3406
3481
|
"stripe_not_supported",
|
|
3407
3482
|
"missing_required_param",
|
|
3408
|
-
"invalid_param_value"
|
|
3483
|
+
"invalid_param_value",
|
|
3484
|
+
"sdk_version_unsupported"
|
|
3409
3485
|
]);
|
|
3410
3486
|
var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
|
|
3411
3487
|
contractId: "sdk-error-codes-catalogue",
|
|
@@ -3438,39 +3514,6 @@ var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
|
|
|
3438
3514
|
nowMs() - t0
|
|
3439
3515
|
);
|
|
3440
3516
|
}
|
|
3441
|
-
},
|
|
3442
|
-
// Hot-path hook — when the SDK parses a real wire error, assert the code
|
|
3443
|
-
// the customer actually hit carries usable remediation in the shipped
|
|
3444
|
-
// catalogue. Scoped to BACKEND_WIRE_CODES (the contract's set); other
|
|
3445
|
-
// codes are out of scope and pass. This turns the catalogue from a
|
|
3446
|
-
// boot-only completeness claim into a per-error field assertion: a backend
|
|
3447
|
-
// code that ships without remediation is caught the first time a customer
|
|
3448
|
-
// hits it, not only if the off-by-default boot self-test ran.
|
|
3449
|
-
hooks: {
|
|
3450
|
-
onErrorParse(obs) {
|
|
3451
|
-
const t0 = nowMs();
|
|
3452
|
-
const code = obs.errorCode;
|
|
3453
|
-
if (!code || !BACKEND_WIRE_CODES.includes(code)) {
|
|
3454
|
-
return pass(
|
|
3455
|
-
"sdk-error-codes-catalogue",
|
|
3456
|
-
`wire code "${code || "(none)"}" is out of the backend-catalogue scope`,
|
|
3457
|
-
nowMs() - t0
|
|
3458
|
-
);
|
|
3459
|
-
}
|
|
3460
|
-
const entry = getErrorCode(code);
|
|
3461
|
-
if (!entry || !entry.description || entry.description.trim().length === 0 || !entry.resolution || entry.resolution.trim().length === 0) {
|
|
3462
|
-
return fail(
|
|
3463
|
-
"sdk-error-codes-catalogue",
|
|
3464
|
-
`backend wire code "${code}" hit in the field has no description+resolution in the shipped catalogue`,
|
|
3465
|
-
nowMs() - t0
|
|
3466
|
-
);
|
|
3467
|
-
}
|
|
3468
|
-
return pass(
|
|
3469
|
-
"sdk-error-codes-catalogue",
|
|
3470
|
-
`backend wire code "${code}" carries description + resolution`,
|
|
3471
|
-
nowMs() - t0
|
|
3472
|
-
);
|
|
3473
|
-
}
|
|
3474
3517
|
}
|
|
3475
3518
|
};
|
|
3476
3519
|
var STATIC_VERIFIERS = Object.freeze([
|
|
@@ -3594,24 +3637,6 @@ function runOnErrorParse(verifiers, reporter, ctx, obs) {
|
|
|
3594
3637
|
reporter.report(result, "hot_path", "errorParse");
|
|
3595
3638
|
}
|
|
3596
3639
|
}
|
|
3597
|
-
function runOnReportContractFailure(verifiers, reporter, ctx, obs) {
|
|
3598
|
-
if (ctx.disableContractAssertions) return;
|
|
3599
|
-
for (const verifier of verifiers) {
|
|
3600
|
-
const hook = verifier.hooks?.onReportContractFailure;
|
|
3601
|
-
if (!hook) continue;
|
|
3602
|
-
let result;
|
|
3603
|
-
try {
|
|
3604
|
-
result = hook(obs);
|
|
3605
|
-
} catch (err) {
|
|
3606
|
-
result = fail(
|
|
3607
|
-
verifier.contractId,
|
|
3608
|
-
`hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
|
|
3609
|
-
0
|
|
3610
|
-
);
|
|
3611
|
-
}
|
|
3612
|
-
reporter.report(result, "hot_path", "reportContractFailure");
|
|
3613
|
-
}
|
|
3614
|
-
}
|
|
3615
3640
|
function defaultDebugModeFlag() {
|
|
3616
3641
|
try {
|
|
3617
3642
|
if (typeof process !== "undefined" && process.env) {
|
|
@@ -3763,18 +3788,18 @@ function isInAppFrame(filename) {
|
|
|
3763
3788
|
if (/\/crossdeck\.umd\.min\.js$/.test(filename)) return false;
|
|
3764
3789
|
return true;
|
|
3765
3790
|
}
|
|
3766
|
-
function fingerprintError(message, frames,
|
|
3791
|
+
function fingerprintError(message, frames, location2) {
|
|
3767
3792
|
const inAppFrames = frames.filter((f) => f.in_app).slice(0, 3);
|
|
3768
3793
|
const parts = [
|
|
3769
3794
|
(message || "").slice(0, 200),
|
|
3770
3795
|
...inAppFrames.map((f) => `${f.function}@${f.filename}:${f.lineno}`)
|
|
3771
3796
|
];
|
|
3772
|
-
if (inAppFrames.length === 0 &&
|
|
3797
|
+
if (inAppFrames.length === 0 && location2) {
|
|
3773
3798
|
const loc = [
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3799
|
+
location2.errorType ?? "",
|
|
3800
|
+
location2.filename ?? "",
|
|
3801
|
+
location2.lineno ?? "",
|
|
3802
|
+
location2.colno ?? ""
|
|
3778
3803
|
].join(":");
|
|
3779
3804
|
if (loc !== ":::") parts.push(loc);
|
|
3780
3805
|
}
|
|
@@ -4444,9 +4469,6 @@ var CrossdeckClient = class {
|
|
|
4444
4469
|
this.verifiers = null;
|
|
4445
4470
|
this.verifierReporter = null;
|
|
4446
4471
|
this.verifierCtx = null;
|
|
4447
|
-
// The live configured event-flush interval, surfaced to the track-path
|
|
4448
|
-
// verifier so flush-interval-parity validates the real cadence per event.
|
|
4449
|
-
this.flushIntervalMs = 2e3;
|
|
4450
4472
|
}
|
|
4451
4473
|
/**
|
|
4452
4474
|
* Boot the SDK. Idempotent — calling init twice with the same options
|
|
@@ -4616,6 +4638,13 @@ var CrossdeckClient = class {
|
|
|
4616
4638
|
headline,
|
|
4617
4639
|
{ ...info }
|
|
4618
4640
|
);
|
|
4641
|
+
},
|
|
4642
|
+
onParked: (info) => {
|
|
4643
|
+
debug.emit(
|
|
4644
|
+
"sdk.parked",
|
|
4645
|
+
`[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.`,
|
|
4646
|
+
{ ...info }
|
|
4647
|
+
);
|
|
4619
4648
|
}
|
|
4620
4649
|
});
|
|
4621
4650
|
const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
|
|
@@ -4623,6 +4652,7 @@ var CrossdeckClient = class {
|
|
|
4623
4652
|
persistIdentity ? effectiveStorage : new MemoryStorage(),
|
|
4624
4653
|
opts.storagePrefix
|
|
4625
4654
|
);
|
|
4655
|
+
processInternalOptOutUrl();
|
|
4626
4656
|
const consent = new ConsentManager({ respectDnt: options.respectDnt === true });
|
|
4627
4657
|
if (consent.isDntDenied) {
|
|
4628
4658
|
debug.emit(
|
|
@@ -4726,8 +4756,6 @@ var CrossdeckClient = class {
|
|
|
4726
4756
|
this.verifierReporter = new VerifierReporter(this.verifierCtx);
|
|
4727
4757
|
const flushVerifier = buildFlushIntervalVerifier(opts.eventFlushIntervalMs);
|
|
4728
4758
|
this.verifiers = Object.freeze([...STATIC_VERIFIERS, flushVerifier]);
|
|
4729
|
-
this.flushIntervalMs = opts.eventFlushIntervalMs;
|
|
4730
|
-
this.verifierReporter.attachVerifiers(this.verifiers);
|
|
4731
4759
|
if (localDevMode) {
|
|
4732
4760
|
const verifyAtBootLocal = options.verifyContractsAtBoot ?? debugDefault;
|
|
4733
4761
|
if (verifyAtBootLocal && this.verifierReporter) {
|
|
@@ -5278,8 +5306,27 @@ var CrossdeckClient = class {
|
|
|
5278
5306
|
);
|
|
5279
5307
|
}
|
|
5280
5308
|
}
|
|
5309
|
+
const seq = s.autoTracker?.nextSeq() ?? 0;
|
|
5310
|
+
const occurrenceTimestamp = Date.now();
|
|
5281
5311
|
s.autoTracker?.markActivity();
|
|
5282
|
-
const
|
|
5312
|
+
const wireContext = {};
|
|
5313
|
+
const di = s.deviceInfo;
|
|
5314
|
+
if (di.os) wireContext.os = di.os;
|
|
5315
|
+
if (di.osVersion) wireContext.osVersion = di.osVersion;
|
|
5316
|
+
const appVer = s.options.appVersion;
|
|
5317
|
+
if (appVer) wireContext.appVersion = appVer;
|
|
5318
|
+
wireContext.sdkName = SDK_NAME;
|
|
5319
|
+
wireContext.sdkVersion = s.options.sdkVersion;
|
|
5320
|
+
if (di.locale) wireContext.locale = di.locale;
|
|
5321
|
+
if (di.timezone) wireContext.timezone = di.timezone;
|
|
5322
|
+
if (di.browser) wireContext.browser = di.browser;
|
|
5323
|
+
if (di.browserVersion) wireContext.browserVersion = di.browserVersion;
|
|
5324
|
+
const enriched = {};
|
|
5325
|
+
if (di.screenWidth !== void 0) enriched.screenWidth = di.screenWidth;
|
|
5326
|
+
if (di.screenHeight !== void 0) enriched.screenHeight = di.screenHeight;
|
|
5327
|
+
if (di.viewportWidth !== void 0) enriched.viewportWidth = di.viewportWidth;
|
|
5328
|
+
if (di.viewportHeight !== void 0) enriched.viewportHeight = di.viewportHeight;
|
|
5329
|
+
if (di.devicePixelRatio !== void 0) enriched.devicePixelRatio = di.devicePixelRatio;
|
|
5283
5330
|
const sessionId = s.autoTracker?.currentSessionId;
|
|
5284
5331
|
if (sessionId) enriched.sessionId = sessionId;
|
|
5285
5332
|
const pageviewId = s.autoTracker?.currentPageviewId;
|
|
@@ -5302,17 +5349,24 @@ var CrossdeckClient = class {
|
|
|
5302
5349
|
}
|
|
5303
5350
|
}
|
|
5304
5351
|
const supers = s.superProps.getSuperProperties();
|
|
5305
|
-
Object.
|
|
5352
|
+
for (const k of Object.keys(supers)) {
|
|
5353
|
+
if (!(k in enriched)) enriched[k] = supers[k];
|
|
5354
|
+
}
|
|
5306
5355
|
const groupIds = s.superProps.getGroupIds();
|
|
5307
5356
|
if (Object.keys(groupIds).length > 0) {
|
|
5308
5357
|
enriched.$groups = groupIds;
|
|
5309
5358
|
}
|
|
5310
5359
|
Object.assign(enriched, validation.properties);
|
|
5360
|
+
if (isInternalOptOut()) {
|
|
5361
|
+
enriched[INTERNAL_OPT_OUT_PROPERTY] = true;
|
|
5362
|
+
}
|
|
5311
5363
|
const finalProperties = s.scrubPii ? scrubPiiFromProperties(enriched) : enriched;
|
|
5312
5364
|
const event = {
|
|
5313
5365
|
eventId: this.mintEventId(),
|
|
5314
5366
|
name,
|
|
5315
|
-
timestamp:
|
|
5367
|
+
timestamp: occurrenceTimestamp,
|
|
5368
|
+
seq,
|
|
5369
|
+
context: wireContext,
|
|
5316
5370
|
properties: finalProperties
|
|
5317
5371
|
};
|
|
5318
5372
|
Object.assign(event, this.identityHintForEvent());
|
|
@@ -5322,8 +5376,7 @@ var CrossdeckClient = class {
|
|
|
5322
5376
|
callerProperties: validation.properties,
|
|
5323
5377
|
superProperties: supers,
|
|
5324
5378
|
deviceProperties: s.deviceInfo,
|
|
5325
|
-
mergedProperties: enriched
|
|
5326
|
-
flushIntervalMs: this.flushIntervalMs
|
|
5379
|
+
mergedProperties: enriched
|
|
5327
5380
|
});
|
|
5328
5381
|
}
|
|
5329
5382
|
if (!isError && !isWebVital) {
|