@cross-deck/web 1.2.0 → 1.3.1
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 +64 -1
- package/dist/crossdeck.umd.min.js +2 -2
- package/dist/crossdeck.umd.min.js.map +1 -1
- package/dist/error-codes.json +1 -1
- package/dist/index.cjs +212 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.mjs +212 -55
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +212 -55
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +212 -55
- package/dist/react.mjs.map +1 -1
- package/dist/vue.cjs +212 -55
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.mjs +212 -55
- package/dist/vue.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react.mjs
CHANGED
|
@@ -66,9 +66,11 @@ function typeMapForStatus(status) {
|
|
|
66
66
|
return "internal_error";
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
// src/
|
|
69
|
+
// src/_version.ts
|
|
70
|
+
var SDK_VERSION = "1.3.0";
|
|
70
71
|
var SDK_NAME = "@cross-deck/web";
|
|
71
|
-
|
|
72
|
+
|
|
73
|
+
// src/http.ts
|
|
72
74
|
var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
73
75
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
74
76
|
var HttpClient = class {
|
|
@@ -499,8 +501,10 @@ function computeNextDelay(attempts, retryAfterMs, options = {}, random = Math.ra
|
|
|
499
501
|
const safeAttempts = Math.min(attempts, 30);
|
|
500
502
|
const ceiling = Math.min(max, base * Math.pow(factor, safeAttempts));
|
|
501
503
|
const jittered = ceiling * random();
|
|
502
|
-
if (retryAfterMs !== void 0
|
|
503
|
-
|
|
504
|
+
if (retryAfterMs !== void 0) {
|
|
505
|
+
const ABSOLUTE_MAX_MS = 24 * 60 * 60 * 1e3;
|
|
506
|
+
const honoured = Math.min(ABSOLUTE_MAX_MS, retryAfterMs);
|
|
507
|
+
if (honoured > jittered) return honoured;
|
|
504
508
|
}
|
|
505
509
|
return Math.max(0, Math.round(jittered));
|
|
506
510
|
}
|
|
@@ -535,6 +539,27 @@ var EventQueue = class {
|
|
|
535
539
|
constructor(cfg) {
|
|
536
540
|
this.cfg = cfg;
|
|
537
541
|
this.buffer = [];
|
|
542
|
+
/**
|
|
543
|
+
* In-flight events that have been spliced from `buffer` for the
|
|
544
|
+
* current batch but haven't yet been confirmed (success or permanent
|
|
545
|
+
* failure). On a retry-driven re-flush we re-use this slot alongside
|
|
546
|
+
* `pendingBatchId` so the Stripe-style Idempotency-Key is preserved
|
|
547
|
+
* across retries of the SAME logical batch.
|
|
548
|
+
*
|
|
549
|
+
* Pre-fix the splice cleared the buffer AND we immediately
|
|
550
|
+
* `persistent.save(empty)` BEFORE awaiting the network call — a
|
|
551
|
+
* crash mid-flight wiped the persisted blob and the batch was lost
|
|
552
|
+
* with no replay on next boot. Now the persisted blob always carries
|
|
553
|
+
* `[...pendingBatch, ...buffer]` so the in-flight events survive
|
|
554
|
+
* until the server confirms them.
|
|
555
|
+
*
|
|
556
|
+
* Pre-fix every retry attempt also minted a NEW batchId via
|
|
557
|
+
* `splice + mintBatchId`, defeating the backend's
|
|
558
|
+
* `Idempotency-Key` dedup. Reuse via this slot brings web in
|
|
559
|
+
* lockstep with node (which already had the field).
|
|
560
|
+
*/
|
|
561
|
+
this.pendingBatch = null;
|
|
562
|
+
this.pendingBatchId = null;
|
|
538
563
|
this.dropped = 0;
|
|
539
564
|
this.inFlight = 0;
|
|
540
565
|
this.lastFlushAt = 0;
|
|
@@ -567,7 +592,7 @@ var EventQueue = class {
|
|
|
567
592
|
this.cfg.onDrop?.(overflow);
|
|
568
593
|
}
|
|
569
594
|
this.cfg.onBufferChange?.(this.buffer.length);
|
|
570
|
-
this.
|
|
595
|
+
this.persistAll();
|
|
571
596
|
if (this.buffer.length >= this.cfg.batchSize) {
|
|
572
597
|
void this.flush();
|
|
573
598
|
} else {
|
|
@@ -576,22 +601,44 @@ var EventQueue = class {
|
|
|
576
601
|
}
|
|
577
602
|
/**
|
|
578
603
|
* Flush the buffer to /v1/events. Resolves when the network call
|
|
579
|
-
* completes (success or failure). On
|
|
580
|
-
*
|
|
604
|
+
* completes (success or failure). On a retryable failure the batch
|
|
605
|
+
* stays in `pendingBatch` for the next scheduled retry — the SAME
|
|
606
|
+
* batch with the SAME `Idempotency-Key` is re-sent (Stripe pattern).
|
|
607
|
+
*
|
|
608
|
+
* Three terminal states from one call:
|
|
609
|
+
* - 2xx success: pendingBatch cleared, persisted state collapses to
|
|
610
|
+
* just `buffer` (any new events that arrived during in-flight).
|
|
611
|
+
* - 4xx permanent (except 408/429): pendingBatch DROPPED, `dropped`
|
|
612
|
+
* counter increments, a `permanent_failure` signal fires. The
|
|
613
|
+
* server is telling us our request is malformed / our key is
|
|
614
|
+
* revoked / we lack permission — retrying with the same key
|
|
615
|
+
* forever just grows the queue while the customer thinks events
|
|
616
|
+
* are landing.
|
|
617
|
+
* - 5xx / network / 408 / 429: pendingBatch + batchId stay; backoff
|
|
618
|
+
* schedules a retry; the next `flush()` re-uses both.
|
|
581
619
|
*
|
|
582
620
|
* `options.keepalive` marks the underlying fetch as keepalive so the
|
|
583
|
-
* browser keeps the request alive past page unload. Use
|
|
584
|
-
*
|
|
621
|
+
* browser keeps the request alive past page unload. Use for terminal
|
|
622
|
+
* flushes (pagehide / visibilitychange→hidden / beforeunload).
|
|
585
623
|
*/
|
|
586
624
|
async flush(options = {}) {
|
|
587
|
-
|
|
625
|
+
let batch;
|
|
626
|
+
let batchId;
|
|
627
|
+
if (this.pendingBatch !== null && this.pendingBatchId !== null) {
|
|
628
|
+
batch = this.pendingBatch;
|
|
629
|
+
batchId = this.pendingBatchId;
|
|
630
|
+
} else {
|
|
631
|
+
if (this.buffer.length === 0) return null;
|
|
632
|
+
batch = this.buffer.splice(0);
|
|
633
|
+
batchId = this.mintBatchId();
|
|
634
|
+
this.pendingBatch = batch;
|
|
635
|
+
this.pendingBatchId = batchId;
|
|
636
|
+
this.inFlight += batch.length;
|
|
637
|
+
this.cfg.onBufferChange?.(this.buffer.length);
|
|
638
|
+
this.persistAll();
|
|
639
|
+
}
|
|
588
640
|
this.cancelTimerIfSet();
|
|
589
641
|
this.nextRetryAt = null;
|
|
590
|
-
const batch = this.buffer.splice(0);
|
|
591
|
-
const batchId = this.mintBatchId();
|
|
592
|
-
this.inFlight += batch.length;
|
|
593
|
-
this.persistent?.save(this.buffer);
|
|
594
|
-
this.cfg.onBufferChange?.(this.buffer.length);
|
|
595
642
|
try {
|
|
596
643
|
const env = this.cfg.envelope();
|
|
597
644
|
const result = await this.cfg.http.request("POST", "/events", {
|
|
@@ -610,20 +657,33 @@ var EventQueue = class {
|
|
|
610
657
|
this.lastFlushAt = Date.now();
|
|
611
658
|
this.lastError = null;
|
|
612
659
|
this.inFlight -= batch.length;
|
|
660
|
+
this.pendingBatch = null;
|
|
661
|
+
this.pendingBatchId = null;
|
|
613
662
|
this.retry.recordSuccess();
|
|
614
|
-
this.
|
|
663
|
+
this.persistAll();
|
|
615
664
|
if (!this.firstFlushFired) {
|
|
616
665
|
this.firstFlushFired = true;
|
|
617
666
|
this.cfg.onFirstFlushSuccess?.();
|
|
618
667
|
}
|
|
619
668
|
return result;
|
|
620
669
|
} catch (err) {
|
|
621
|
-
this.buffer.unshift(...batch);
|
|
622
|
-
this.inFlight -= batch.length;
|
|
623
670
|
const message = err instanceof Error ? err.message : String(err);
|
|
624
671
|
this.lastError = message;
|
|
625
|
-
|
|
626
|
-
|
|
672
|
+
if (isPermanent4xx(err)) {
|
|
673
|
+
const droppedCount = batch.length;
|
|
674
|
+
this.pendingBatch = null;
|
|
675
|
+
this.pendingBatchId = null;
|
|
676
|
+
this.inFlight -= droppedCount;
|
|
677
|
+
this.dropped += droppedCount;
|
|
678
|
+
this.persistAll();
|
|
679
|
+
this.cfg.onDrop?.(droppedCount);
|
|
680
|
+
this.cfg.onPermanentFailure?.({
|
|
681
|
+
status: err.status ?? 0,
|
|
682
|
+
droppedCount,
|
|
683
|
+
lastError: message
|
|
684
|
+
});
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
627
687
|
const retryAfterMs = extractRetryAfterMs(err);
|
|
628
688
|
const delay = this.retry.nextDelay(retryAfterMs);
|
|
629
689
|
this.scheduleRetry(delay);
|
|
@@ -641,6 +701,8 @@ var EventQueue = class {
|
|
|
641
701
|
this.cancelTimerIfSet();
|
|
642
702
|
this.nextRetryAt = null;
|
|
643
703
|
this.buffer = [];
|
|
704
|
+
this.pendingBatch = null;
|
|
705
|
+
this.pendingBatchId = null;
|
|
644
706
|
this.dropped = 0;
|
|
645
707
|
this.inFlight = 0;
|
|
646
708
|
this.lastError = null;
|
|
@@ -650,6 +712,10 @@ var EventQueue = class {
|
|
|
650
712
|
}
|
|
651
713
|
getStats() {
|
|
652
714
|
return {
|
|
715
|
+
// `buffered` counts events waiting for their FIRST flush. The
|
|
716
|
+
// in-flight pendingBatch (retrying) is tracked separately via
|
|
717
|
+
// `inFlight` — surfacing both lets diagnostics show "we have
|
|
718
|
+
// events stuck retrying" distinct from "new events arriving".
|
|
653
719
|
buffered: this.buffer.length,
|
|
654
720
|
dropped: this.dropped,
|
|
655
721
|
inFlight: this.inFlight,
|
|
@@ -659,6 +725,29 @@ var EventQueue = class {
|
|
|
659
725
|
nextRetryAt: this.nextRetryAt
|
|
660
726
|
};
|
|
661
727
|
}
|
|
728
|
+
/**
|
|
729
|
+
* The Idempotency-Key of the in-flight pending batch (if any).
|
|
730
|
+
* Exposed for testing the Stripe-style retry-reuse contract.
|
|
731
|
+
* Production callers don't need this.
|
|
732
|
+
*/
|
|
733
|
+
get pendingIdempotencyKey() {
|
|
734
|
+
return this.pendingBatchId;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Persist `[...pendingBatch, ...buffer]` — the full set of
|
|
738
|
+
* not-yet-confirmed events. The next boot rehydrates this exact set
|
|
739
|
+
* into `buffer` and replays. The server dedups via eventId
|
|
740
|
+
* (ReplacingMergeTree on the warehouse side), so re-sending an event
|
|
741
|
+
* that may have already landed is safe.
|
|
742
|
+
*/
|
|
743
|
+
persistAll() {
|
|
744
|
+
if (!this.persistent) return;
|
|
745
|
+
if (this.pendingBatch === null) {
|
|
746
|
+
this.persistent.save(this.buffer);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
this.persistent.save([...this.pendingBatch, ...this.buffer]);
|
|
750
|
+
}
|
|
662
751
|
// ---------- internal scheduling ----------
|
|
663
752
|
scheduleIdleFlush() {
|
|
664
753
|
this.cancelTimerIfSet();
|
|
@@ -692,6 +781,14 @@ function extractRetryAfterMs(err) {
|
|
|
692
781
|
}
|
|
693
782
|
return void 0;
|
|
694
783
|
}
|
|
784
|
+
function isPermanent4xx(err) {
|
|
785
|
+
if (!err || typeof err !== "object") return false;
|
|
786
|
+
const status = err.status;
|
|
787
|
+
if (typeof status !== "number" || !Number.isFinite(status)) return false;
|
|
788
|
+
if (status < 400 || status >= 500) return false;
|
|
789
|
+
if (status === 408 || status === 429) return false;
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
695
792
|
function defaultScheduler(fn, ms) {
|
|
696
793
|
const id = setTimeout(fn, ms);
|
|
697
794
|
if (typeof id.unref === "function") {
|
|
@@ -1033,6 +1130,7 @@ var AutoTracker = class {
|
|
|
1033
1130
|
/** Exposed for tests + consumers that want to reset the session manually. */
|
|
1034
1131
|
resetSession() {
|
|
1035
1132
|
if (this.session && !this.session.endedSent) this.emitSessionEnd();
|
|
1133
|
+
this.pageviewId = null;
|
|
1036
1134
|
this.session = this.startNewSession();
|
|
1037
1135
|
this.emitSessionStart();
|
|
1038
1136
|
}
|
|
@@ -1069,6 +1167,7 @@ var AutoTracker = class {
|
|
|
1069
1167
|
const hiddenFor = this.session.hiddenAt ? Date.now() - this.session.hiddenAt : 0;
|
|
1070
1168
|
if (hiddenFor >= SESSION_RESUME_THRESHOLD_MS) {
|
|
1071
1169
|
this.emitSessionEnd();
|
|
1170
|
+
this.pageviewId = null;
|
|
1072
1171
|
this.session = this.startNewSession();
|
|
1073
1172
|
this.emitSessionStart();
|
|
1074
1173
|
} else {
|
|
@@ -1442,7 +1541,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1442
1541
|
const maxStringLength = options.maxStringLength ?? DEFAULT_MAX_STRING;
|
|
1443
1542
|
const maxBatchPropertyBytes = options.maxBatchPropertyBytes ?? DEFAULT_MAX_BYTES;
|
|
1444
1543
|
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
1445
|
-
const seen = /* @__PURE__ */ new
|
|
1544
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1446
1545
|
const visit = (value, key, depth) => {
|
|
1447
1546
|
if (depth > maxDepth) {
|
|
1448
1547
|
warnings.push({ kind: "depth_exceeded", key });
|
|
@@ -1530,6 +1629,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1530
1629
|
const result = visit(value[i], `${key}[${i}]`, depth + 1);
|
|
1531
1630
|
if (result.keep) out.push(result.value);
|
|
1532
1631
|
}
|
|
1632
|
+
seen.delete(value);
|
|
1533
1633
|
return { keep: true, value: out };
|
|
1534
1634
|
}
|
|
1535
1635
|
if (t === "object") {
|
|
@@ -1544,6 +1644,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1544
1644
|
const result = visit(obj[k], `${key}.${k}`, depth + 1);
|
|
1545
1645
|
if (result.keep) out[k] = result.value;
|
|
1546
1646
|
}
|
|
1647
|
+
seen.delete(obj);
|
|
1547
1648
|
return { keep: true, value: out };
|
|
1548
1649
|
}
|
|
1549
1650
|
warnings.push({ kind: "non_serialisable", key });
|
|
@@ -1886,35 +1987,27 @@ var ConsentManager = class {
|
|
|
1886
1987
|
};
|
|
1887
1988
|
var EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
1888
1989
|
var CARD_PATTERN = /\b\d(?:[ -]?\d){12,18}\b/g;
|
|
1889
|
-
var REPLACEMENT_EMAIL = "
|
|
1890
|
-
var REPLACEMENT_CARD = "
|
|
1990
|
+
var REPLACEMENT_EMAIL = "<email>";
|
|
1991
|
+
var REPLACEMENT_CARD = "<card>";
|
|
1891
1992
|
function scrubPii(value) {
|
|
1892
1993
|
if (!value) return value;
|
|
1893
|
-
|
|
1894
|
-
if (EMAIL_PATTERN.test(out)) {
|
|
1895
|
-
out = out.replace(EMAIL_PATTERN, REPLACEMENT_EMAIL);
|
|
1896
|
-
}
|
|
1897
|
-
EMAIL_PATTERN.lastIndex = 0;
|
|
1898
|
-
if (CARD_PATTERN.test(out)) {
|
|
1899
|
-
out = out.replace(CARD_PATTERN, REPLACEMENT_CARD);
|
|
1900
|
-
}
|
|
1901
|
-
CARD_PATTERN.lastIndex = 0;
|
|
1902
|
-
return out;
|
|
1994
|
+
return value.replace(EMAIL_PATTERN, REPLACEMENT_EMAIL).replace(CARD_PATTERN, REPLACEMENT_CARD);
|
|
1903
1995
|
}
|
|
1904
1996
|
function scrubPiiFromProperties(properties) {
|
|
1905
1997
|
const out = {};
|
|
1906
1998
|
for (const k of Object.keys(properties)) {
|
|
1907
|
-
|
|
1908
|
-
if (typeof v === "string") {
|
|
1909
|
-
out[k] = scrubPii(v);
|
|
1910
|
-
} else if (Array.isArray(v)) {
|
|
1911
|
-
out[k] = v.map((item) => typeof item === "string" ? scrubPii(item) : item);
|
|
1912
|
-
} else {
|
|
1913
|
-
out[k] = v;
|
|
1914
|
-
}
|
|
1999
|
+
out[k] = scrubValue(properties[k]);
|
|
1915
2000
|
}
|
|
1916
2001
|
return out;
|
|
1917
2002
|
}
|
|
2003
|
+
function scrubValue(v) {
|
|
2004
|
+
if (typeof v === "string") return scrubPii(v);
|
|
2005
|
+
if (Array.isArray(v)) return v.map(scrubValue);
|
|
2006
|
+
if (v && typeof v === "object" && v.constructor === Object) {
|
|
2007
|
+
return scrubPiiFromProperties(v);
|
|
2008
|
+
}
|
|
2009
|
+
return v;
|
|
2010
|
+
}
|
|
1918
2011
|
|
|
1919
2012
|
// src/breadcrumbs.ts
|
|
1920
2013
|
var BreadcrumbBuffer = class {
|
|
@@ -2204,16 +2297,18 @@ var ErrorTracker = class {
|
|
|
2204
2297
|
const url = typeof input === "string" ? input : input?.url ?? "";
|
|
2205
2298
|
const method = (init.method || "GET").toUpperCase();
|
|
2206
2299
|
const start = Date.now();
|
|
2207
|
-
this.opts.
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2300
|
+
if (!isSelfRequest(url, this.opts.selfHostname)) {
|
|
2301
|
+
this.opts.breadcrumbs.add({
|
|
2302
|
+
timestamp: start,
|
|
2303
|
+
category: "http",
|
|
2304
|
+
message: `${method} ${url}`,
|
|
2305
|
+
data: { url, method }
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2213
2308
|
try {
|
|
2214
2309
|
const response = await origFetch(...args);
|
|
2215
2310
|
if (response.status >= 500 && this.opts.isConsented()) {
|
|
2216
|
-
if (!url.
|
|
2311
|
+
if (!isSelfRequest(url, this.opts.selfHostname)) {
|
|
2217
2312
|
this.captureHttp({
|
|
2218
2313
|
url,
|
|
2219
2314
|
method,
|
|
@@ -2262,7 +2357,7 @@ var ErrorTracker = class {
|
|
|
2262
2357
|
try {
|
|
2263
2358
|
if (xhr.status >= 500 && tracker.opts.isConsented()) {
|
|
2264
2359
|
const url = xhr._cdUrl ?? "";
|
|
2265
|
-
if (!url.
|
|
2360
|
+
if (!isSelfRequest(url, tracker.opts.selfHostname)) {
|
|
2266
2361
|
tracker.captureHttp({
|
|
2267
2362
|
url,
|
|
2268
2363
|
method: (xhr._cdMethod ?? "GET").toUpperCase(),
|
|
@@ -2422,9 +2517,10 @@ var ErrorTracker = class {
|
|
|
2422
2517
|
if (!this.passesSample(err)) return;
|
|
2423
2518
|
if (!this.passesRateLimit(err)) return;
|
|
2424
2519
|
let finalErr = err;
|
|
2425
|
-
|
|
2520
|
+
const hook = this.opts.beforeSend?.();
|
|
2521
|
+
if (hook) {
|
|
2426
2522
|
try {
|
|
2427
|
-
finalErr =
|
|
2523
|
+
finalErr = hook(err);
|
|
2428
2524
|
} catch {
|
|
2429
2525
|
finalErr = err;
|
|
2430
2526
|
}
|
|
@@ -2606,6 +2702,22 @@ function safeClone(v) {
|
|
|
2606
2702
|
function safeStringify2(v) {
|
|
2607
2703
|
return coerceErrorPayload(v).message;
|
|
2608
2704
|
}
|
|
2705
|
+
function extractSelfHostname(baseUrl) {
|
|
2706
|
+
if (!baseUrl || typeof baseUrl !== "string") return null;
|
|
2707
|
+
try {
|
|
2708
|
+
return new URL(baseUrl).hostname.toLowerCase();
|
|
2709
|
+
} catch {
|
|
2710
|
+
return null;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
function isSelfRequest(requestUrl, selfHostname) {
|
|
2714
|
+
if (!selfHostname || !requestUrl) return false;
|
|
2715
|
+
try {
|
|
2716
|
+
return new URL(requestUrl).hostname.toLowerCase() === selfHostname;
|
|
2717
|
+
} catch {
|
|
2718
|
+
return false;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2609
2721
|
|
|
2610
2722
|
// src/crossdeck.ts
|
|
2611
2723
|
var CrossdeckClient = class {
|
|
@@ -2622,6 +2734,24 @@ var CrossdeckClient = class {
|
|
|
2622
2734
|
* mismatched env fails fast at boot rather than at first event-flush.
|
|
2623
2735
|
*/
|
|
2624
2736
|
init(options) {
|
|
2737
|
+
if (this.state) {
|
|
2738
|
+
try {
|
|
2739
|
+
this.state.uninstallUnloadFlush?.();
|
|
2740
|
+
} catch {
|
|
2741
|
+
}
|
|
2742
|
+
try {
|
|
2743
|
+
this.state.autoTracker?.uninstall();
|
|
2744
|
+
} catch {
|
|
2745
|
+
}
|
|
2746
|
+
try {
|
|
2747
|
+
this.state.webVitals?.uninstall();
|
|
2748
|
+
} catch {
|
|
2749
|
+
}
|
|
2750
|
+
try {
|
|
2751
|
+
this.state.errors?.uninstall();
|
|
2752
|
+
} catch {
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2625
2755
|
if (!options.publicKey || !options.publicKey.startsWith("cd_pub_")) {
|
|
2626
2756
|
throw new CrossdeckError({
|
|
2627
2757
|
type: "configuration_error",
|
|
@@ -2729,6 +2859,15 @@ var CrossdeckClient = class {
|
|
|
2729
2859
|
`Event flush failed (${info.lastError}). Retrying in ${info.delayMs}ms (attempt ${info.consecutiveFailures}).`,
|
|
2730
2860
|
{ ...info }
|
|
2731
2861
|
);
|
|
2862
|
+
},
|
|
2863
|
+
onPermanentFailure: (info) => {
|
|
2864
|
+
const headline = `[crossdeck] Event batch DROPPED (status ${info.status}): ${info.lastError}. ${info.droppedCount} event(s) lost \u2014 check your publishable key + app config.`;
|
|
2865
|
+
console.error(headline);
|
|
2866
|
+
debug.emit(
|
|
2867
|
+
"sdk.flush_permanent_failure",
|
|
2868
|
+
headline,
|
|
2869
|
+
{ ...info }
|
|
2870
|
+
);
|
|
2732
2871
|
}
|
|
2733
2872
|
});
|
|
2734
2873
|
const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
|
|
@@ -2795,8 +2934,20 @@ var CrossdeckClient = class {
|
|
|
2795
2934
|
report: (err) => this.reportError(err),
|
|
2796
2935
|
getContext: () => ({ ...this.state.errorContext }),
|
|
2797
2936
|
getTags: () => ({ ...this.state.errorTags }),
|
|
2798
|
-
|
|
2799
|
-
|
|
2937
|
+
// GETTER, not a captured value — `setErrorBeforeSend()` mutates
|
|
2938
|
+
// `state.errorBeforeSend` after init() and the tracker MUST
|
|
2939
|
+
// pick up the new hook on the next error. The pre-fix shape
|
|
2940
|
+
// (`beforeSend: this.state!.errorBeforeSend`) snapshotted
|
|
2941
|
+
// `null` at construction and made the customer's PII scrubber
|
|
2942
|
+
// silently inert. See error-capture.ts:ErrorTrackerOptions.beforeSend.
|
|
2943
|
+
beforeSend: () => this.state.errorBeforeSend,
|
|
2944
|
+
isConsented: () => this.state.consent.errors,
|
|
2945
|
+
// Derived from the configured baseUrl at init() time. Used by
|
|
2946
|
+
// the fetch / XHR wrappers to skip captureHttp on Crossdeck's
|
|
2947
|
+
// own requests — pre-fix the skip was hardcoded to
|
|
2948
|
+
// `api.cross-deck.com` and broke for customers on staging /
|
|
2949
|
+
// regional / self-hosted base URLs (recursive capture loop).
|
|
2950
|
+
selfHostname: extractSelfHostname(opts.baseUrl)
|
|
2800
2951
|
});
|
|
2801
2952
|
this.state.errors = tracker;
|
|
2802
2953
|
tracker.install();
|
|
@@ -2879,7 +3030,8 @@ var CrossdeckClient = class {
|
|
|
2879
3030
|
body
|
|
2880
3031
|
});
|
|
2881
3032
|
const priorCdcust = s.identity.crossdeckCustomerId;
|
|
2882
|
-
|
|
3033
|
+
const cacheHasEntries = s.entitlements.list().length > 0;
|
|
3034
|
+
if (priorCdcust && result.crossdeckCustomerId && priorCdcust !== result.crossdeckCustomerId || !priorCdcust && cacheHasEntries) {
|
|
2883
3035
|
s.entitlements.clear();
|
|
2884
3036
|
}
|
|
2885
3037
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
@@ -3321,8 +3473,9 @@ var CrossdeckClient = class {
|
|
|
3321
3473
|
message: "syncPurchases requires a signedTransactionInfo string from StoreKit 2."
|
|
3322
3474
|
});
|
|
3323
3475
|
}
|
|
3476
|
+
const rail = input.rail ?? "apple";
|
|
3324
3477
|
const result = await s.http.request("POST", "/purchases/sync", {
|
|
3325
|
-
body: {
|
|
3478
|
+
body: { ...input, rail }
|
|
3326
3479
|
});
|
|
3327
3480
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
3328
3481
|
s.entitlements.setFromList(result.entitlements);
|
|
@@ -3388,6 +3541,8 @@ var CrossdeckClient = class {
|
|
|
3388
3541
|
this.state.errorContext = {};
|
|
3389
3542
|
this.state.errorTags = {};
|
|
3390
3543
|
this.state.developerUserId = null;
|
|
3544
|
+
this.state.lastServerTime = null;
|
|
3545
|
+
this.state.lastClientTime = null;
|
|
3391
3546
|
if (this.state.autoTracker) {
|
|
3392
3547
|
const tracker = new AutoTracker(
|
|
3393
3548
|
this.state.options.autoTrack,
|
|
@@ -3516,7 +3671,9 @@ function isLocalHostname() {
|
|
|
3516
3671
|
const hostname = w?.location?.hostname;
|
|
3517
3672
|
if (!hostname) return false;
|
|
3518
3673
|
if (hostname === "localhost" || hostname === "127.0.0.1") return true;
|
|
3674
|
+
if (hostname === "0.0.0.0") return true;
|
|
3519
3675
|
if (hostname === "::1" || hostname === "[::1]") return true;
|
|
3676
|
+
if (/^\[?fe80::/i.test(hostname)) return true;
|
|
3520
3677
|
if (hostname.endsWith(".local")) return true;
|
|
3521
3678
|
if (/^10\./.test(hostname)) return true;
|
|
3522
3679
|
if (/^192\.168\./.test(hostname)) return true;
|