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