@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.cjs
CHANGED
|
@@ -92,9 +92,11 @@ function typeMapForStatus(status) {
|
|
|
92
92
|
return "internal_error";
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
// src/
|
|
95
|
+
// src/_version.ts
|
|
96
|
+
var SDK_VERSION = "1.3.0";
|
|
96
97
|
var SDK_NAME = "@cross-deck/web";
|
|
97
|
-
|
|
98
|
+
|
|
99
|
+
// src/http.ts
|
|
98
100
|
var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
99
101
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
100
102
|
var HttpClient = class {
|
|
@@ -525,8 +527,10 @@ function computeNextDelay(attempts, retryAfterMs, options = {}, random = Math.ra
|
|
|
525
527
|
const safeAttempts = Math.min(attempts, 30);
|
|
526
528
|
const ceiling = Math.min(max, base * Math.pow(factor, safeAttempts));
|
|
527
529
|
const jittered = ceiling * random();
|
|
528
|
-
if (retryAfterMs !== void 0
|
|
529
|
-
|
|
530
|
+
if (retryAfterMs !== void 0) {
|
|
531
|
+
const ABSOLUTE_MAX_MS = 24 * 60 * 60 * 1e3;
|
|
532
|
+
const honoured = Math.min(ABSOLUTE_MAX_MS, retryAfterMs);
|
|
533
|
+
if (honoured > jittered) return honoured;
|
|
530
534
|
}
|
|
531
535
|
return Math.max(0, Math.round(jittered));
|
|
532
536
|
}
|
|
@@ -561,6 +565,27 @@ var EventQueue = class {
|
|
|
561
565
|
constructor(cfg) {
|
|
562
566
|
this.cfg = cfg;
|
|
563
567
|
this.buffer = [];
|
|
568
|
+
/**
|
|
569
|
+
* In-flight events that have been spliced from `buffer` for the
|
|
570
|
+
* current batch but haven't yet been confirmed (success or permanent
|
|
571
|
+
* failure). On a retry-driven re-flush we re-use this slot alongside
|
|
572
|
+
* `pendingBatchId` so the Stripe-style Idempotency-Key is preserved
|
|
573
|
+
* across retries of the SAME logical batch.
|
|
574
|
+
*
|
|
575
|
+
* Pre-fix the splice cleared the buffer AND we immediately
|
|
576
|
+
* `persistent.save(empty)` BEFORE awaiting the network call — a
|
|
577
|
+
* crash mid-flight wiped the persisted blob and the batch was lost
|
|
578
|
+
* with no replay on next boot. Now the persisted blob always carries
|
|
579
|
+
* `[...pendingBatch, ...buffer]` so the in-flight events survive
|
|
580
|
+
* until the server confirms them.
|
|
581
|
+
*
|
|
582
|
+
* Pre-fix every retry attempt also minted a NEW batchId via
|
|
583
|
+
* `splice + mintBatchId`, defeating the backend's
|
|
584
|
+
* `Idempotency-Key` dedup. Reuse via this slot brings web in
|
|
585
|
+
* lockstep with node (which already had the field).
|
|
586
|
+
*/
|
|
587
|
+
this.pendingBatch = null;
|
|
588
|
+
this.pendingBatchId = null;
|
|
564
589
|
this.dropped = 0;
|
|
565
590
|
this.inFlight = 0;
|
|
566
591
|
this.lastFlushAt = 0;
|
|
@@ -593,7 +618,7 @@ var EventQueue = class {
|
|
|
593
618
|
this.cfg.onDrop?.(overflow);
|
|
594
619
|
}
|
|
595
620
|
this.cfg.onBufferChange?.(this.buffer.length);
|
|
596
|
-
this.
|
|
621
|
+
this.persistAll();
|
|
597
622
|
if (this.buffer.length >= this.cfg.batchSize) {
|
|
598
623
|
void this.flush();
|
|
599
624
|
} else {
|
|
@@ -602,22 +627,44 @@ var EventQueue = class {
|
|
|
602
627
|
}
|
|
603
628
|
/**
|
|
604
629
|
* Flush the buffer to /v1/events. Resolves when the network call
|
|
605
|
-
* completes (success or failure). On
|
|
606
|
-
*
|
|
630
|
+
* completes (success or failure). On a retryable failure the batch
|
|
631
|
+
* stays in `pendingBatch` for the next scheduled retry — the SAME
|
|
632
|
+
* batch with the SAME `Idempotency-Key` is re-sent (Stripe pattern).
|
|
633
|
+
*
|
|
634
|
+
* Three terminal states from one call:
|
|
635
|
+
* - 2xx success: pendingBatch cleared, persisted state collapses to
|
|
636
|
+
* just `buffer` (any new events that arrived during in-flight).
|
|
637
|
+
* - 4xx permanent (except 408/429): pendingBatch DROPPED, `dropped`
|
|
638
|
+
* counter increments, a `permanent_failure` signal fires. The
|
|
639
|
+
* server is telling us our request is malformed / our key is
|
|
640
|
+
* revoked / we lack permission — retrying with the same key
|
|
641
|
+
* forever just grows the queue while the customer thinks events
|
|
642
|
+
* are landing.
|
|
643
|
+
* - 5xx / network / 408 / 429: pendingBatch + batchId stay; backoff
|
|
644
|
+
* schedules a retry; the next `flush()` re-uses both.
|
|
607
645
|
*
|
|
608
646
|
* `options.keepalive` marks the underlying fetch as keepalive so the
|
|
609
|
-
* browser keeps the request alive past page unload. Use
|
|
610
|
-
*
|
|
647
|
+
* browser keeps the request alive past page unload. Use for terminal
|
|
648
|
+
* flushes (pagehide / visibilitychange→hidden / beforeunload).
|
|
611
649
|
*/
|
|
612
650
|
async flush(options = {}) {
|
|
613
|
-
|
|
651
|
+
let batch;
|
|
652
|
+
let batchId;
|
|
653
|
+
if (this.pendingBatch !== null && this.pendingBatchId !== null) {
|
|
654
|
+
batch = this.pendingBatch;
|
|
655
|
+
batchId = this.pendingBatchId;
|
|
656
|
+
} else {
|
|
657
|
+
if (this.buffer.length === 0) return null;
|
|
658
|
+
batch = this.buffer.splice(0);
|
|
659
|
+
batchId = this.mintBatchId();
|
|
660
|
+
this.pendingBatch = batch;
|
|
661
|
+
this.pendingBatchId = batchId;
|
|
662
|
+
this.inFlight += batch.length;
|
|
663
|
+
this.cfg.onBufferChange?.(this.buffer.length);
|
|
664
|
+
this.persistAll();
|
|
665
|
+
}
|
|
614
666
|
this.cancelTimerIfSet();
|
|
615
667
|
this.nextRetryAt = null;
|
|
616
|
-
const batch = this.buffer.splice(0);
|
|
617
|
-
const batchId = this.mintBatchId();
|
|
618
|
-
this.inFlight += batch.length;
|
|
619
|
-
this.persistent?.save(this.buffer);
|
|
620
|
-
this.cfg.onBufferChange?.(this.buffer.length);
|
|
621
668
|
try {
|
|
622
669
|
const env = this.cfg.envelope();
|
|
623
670
|
const result = await this.cfg.http.request("POST", "/events", {
|
|
@@ -636,20 +683,33 @@ var EventQueue = class {
|
|
|
636
683
|
this.lastFlushAt = Date.now();
|
|
637
684
|
this.lastError = null;
|
|
638
685
|
this.inFlight -= batch.length;
|
|
686
|
+
this.pendingBatch = null;
|
|
687
|
+
this.pendingBatchId = null;
|
|
639
688
|
this.retry.recordSuccess();
|
|
640
|
-
this.
|
|
689
|
+
this.persistAll();
|
|
641
690
|
if (!this.firstFlushFired) {
|
|
642
691
|
this.firstFlushFired = true;
|
|
643
692
|
this.cfg.onFirstFlushSuccess?.();
|
|
644
693
|
}
|
|
645
694
|
return result;
|
|
646
695
|
} catch (err) {
|
|
647
|
-
this.buffer.unshift(...batch);
|
|
648
|
-
this.inFlight -= batch.length;
|
|
649
696
|
const message = err instanceof Error ? err.message : String(err);
|
|
650
697
|
this.lastError = message;
|
|
651
|
-
|
|
652
|
-
|
|
698
|
+
if (isPermanent4xx(err)) {
|
|
699
|
+
const droppedCount = batch.length;
|
|
700
|
+
this.pendingBatch = null;
|
|
701
|
+
this.pendingBatchId = null;
|
|
702
|
+
this.inFlight -= droppedCount;
|
|
703
|
+
this.dropped += droppedCount;
|
|
704
|
+
this.persistAll();
|
|
705
|
+
this.cfg.onDrop?.(droppedCount);
|
|
706
|
+
this.cfg.onPermanentFailure?.({
|
|
707
|
+
status: err.status ?? 0,
|
|
708
|
+
droppedCount,
|
|
709
|
+
lastError: message
|
|
710
|
+
});
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
653
713
|
const retryAfterMs = extractRetryAfterMs(err);
|
|
654
714
|
const delay = this.retry.nextDelay(retryAfterMs);
|
|
655
715
|
this.scheduleRetry(delay);
|
|
@@ -667,6 +727,8 @@ var EventQueue = class {
|
|
|
667
727
|
this.cancelTimerIfSet();
|
|
668
728
|
this.nextRetryAt = null;
|
|
669
729
|
this.buffer = [];
|
|
730
|
+
this.pendingBatch = null;
|
|
731
|
+
this.pendingBatchId = null;
|
|
670
732
|
this.dropped = 0;
|
|
671
733
|
this.inFlight = 0;
|
|
672
734
|
this.lastError = null;
|
|
@@ -676,6 +738,10 @@ var EventQueue = class {
|
|
|
676
738
|
}
|
|
677
739
|
getStats() {
|
|
678
740
|
return {
|
|
741
|
+
// `buffered` counts events waiting for their FIRST flush. The
|
|
742
|
+
// in-flight pendingBatch (retrying) is tracked separately via
|
|
743
|
+
// `inFlight` — surfacing both lets diagnostics show "we have
|
|
744
|
+
// events stuck retrying" distinct from "new events arriving".
|
|
679
745
|
buffered: this.buffer.length,
|
|
680
746
|
dropped: this.dropped,
|
|
681
747
|
inFlight: this.inFlight,
|
|
@@ -685,6 +751,29 @@ var EventQueue = class {
|
|
|
685
751
|
nextRetryAt: this.nextRetryAt
|
|
686
752
|
};
|
|
687
753
|
}
|
|
754
|
+
/**
|
|
755
|
+
* The Idempotency-Key of the in-flight pending batch (if any).
|
|
756
|
+
* Exposed for testing the Stripe-style retry-reuse contract.
|
|
757
|
+
* Production callers don't need this.
|
|
758
|
+
*/
|
|
759
|
+
get pendingIdempotencyKey() {
|
|
760
|
+
return this.pendingBatchId;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Persist `[...pendingBatch, ...buffer]` — the full set of
|
|
764
|
+
* not-yet-confirmed events. The next boot rehydrates this exact set
|
|
765
|
+
* into `buffer` and replays. The server dedups via eventId
|
|
766
|
+
* (ReplacingMergeTree on the warehouse side), so re-sending an event
|
|
767
|
+
* that may have already landed is safe.
|
|
768
|
+
*/
|
|
769
|
+
persistAll() {
|
|
770
|
+
if (!this.persistent) return;
|
|
771
|
+
if (this.pendingBatch === null) {
|
|
772
|
+
this.persistent.save(this.buffer);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
this.persistent.save([...this.pendingBatch, ...this.buffer]);
|
|
776
|
+
}
|
|
688
777
|
// ---------- internal scheduling ----------
|
|
689
778
|
scheduleIdleFlush() {
|
|
690
779
|
this.cancelTimerIfSet();
|
|
@@ -718,6 +807,14 @@ function extractRetryAfterMs(err) {
|
|
|
718
807
|
}
|
|
719
808
|
return void 0;
|
|
720
809
|
}
|
|
810
|
+
function isPermanent4xx(err) {
|
|
811
|
+
if (!err || typeof err !== "object") return false;
|
|
812
|
+
const status = err.status;
|
|
813
|
+
if (typeof status !== "number" || !Number.isFinite(status)) return false;
|
|
814
|
+
if (status < 400 || status >= 500) return false;
|
|
815
|
+
if (status === 408 || status === 429) return false;
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
721
818
|
function defaultScheduler(fn, ms) {
|
|
722
819
|
const id = setTimeout(fn, ms);
|
|
723
820
|
if (typeof id.unref === "function") {
|
|
@@ -1059,6 +1156,7 @@ var AutoTracker = class {
|
|
|
1059
1156
|
/** Exposed for tests + consumers that want to reset the session manually. */
|
|
1060
1157
|
resetSession() {
|
|
1061
1158
|
if (this.session && !this.session.endedSent) this.emitSessionEnd();
|
|
1159
|
+
this.pageviewId = null;
|
|
1062
1160
|
this.session = this.startNewSession();
|
|
1063
1161
|
this.emitSessionStart();
|
|
1064
1162
|
}
|
|
@@ -1095,6 +1193,7 @@ var AutoTracker = class {
|
|
|
1095
1193
|
const hiddenFor = this.session.hiddenAt ? Date.now() - this.session.hiddenAt : 0;
|
|
1096
1194
|
if (hiddenFor >= SESSION_RESUME_THRESHOLD_MS) {
|
|
1097
1195
|
this.emitSessionEnd();
|
|
1196
|
+
this.pageviewId = null;
|
|
1098
1197
|
this.session = this.startNewSession();
|
|
1099
1198
|
this.emitSessionStart();
|
|
1100
1199
|
} else {
|
|
@@ -1468,7 +1567,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1468
1567
|
const maxStringLength = options.maxStringLength ?? DEFAULT_MAX_STRING;
|
|
1469
1568
|
const maxBatchPropertyBytes = options.maxBatchPropertyBytes ?? DEFAULT_MAX_BYTES;
|
|
1470
1569
|
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
1471
|
-
const seen = /* @__PURE__ */ new
|
|
1570
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1472
1571
|
const visit = (value, key, depth) => {
|
|
1473
1572
|
if (depth > maxDepth) {
|
|
1474
1573
|
warnings.push({ kind: "depth_exceeded", key });
|
|
@@ -1556,6 +1655,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1556
1655
|
const result = visit(value[i], `${key}[${i}]`, depth + 1);
|
|
1557
1656
|
if (result.keep) out.push(result.value);
|
|
1558
1657
|
}
|
|
1658
|
+
seen.delete(value);
|
|
1559
1659
|
return { keep: true, value: out };
|
|
1560
1660
|
}
|
|
1561
1661
|
if (t === "object") {
|
|
@@ -1570,6 +1670,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1570
1670
|
const result = visit(obj[k], `${key}.${k}`, depth + 1);
|
|
1571
1671
|
if (result.keep) out[k] = result.value;
|
|
1572
1672
|
}
|
|
1673
|
+
seen.delete(obj);
|
|
1573
1674
|
return { keep: true, value: out };
|
|
1574
1675
|
}
|
|
1575
1676
|
warnings.push({ kind: "non_serialisable", key });
|
|
@@ -1912,35 +2013,27 @@ var ConsentManager = class {
|
|
|
1912
2013
|
};
|
|
1913
2014
|
var EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
1914
2015
|
var CARD_PATTERN = /\b\d(?:[ -]?\d){12,18}\b/g;
|
|
1915
|
-
var REPLACEMENT_EMAIL = "
|
|
1916
|
-
var REPLACEMENT_CARD = "
|
|
2016
|
+
var REPLACEMENT_EMAIL = "<email>";
|
|
2017
|
+
var REPLACEMENT_CARD = "<card>";
|
|
1917
2018
|
function scrubPii(value) {
|
|
1918
2019
|
if (!value) return value;
|
|
1919
|
-
|
|
1920
|
-
if (EMAIL_PATTERN.test(out)) {
|
|
1921
|
-
out = out.replace(EMAIL_PATTERN, REPLACEMENT_EMAIL);
|
|
1922
|
-
}
|
|
1923
|
-
EMAIL_PATTERN.lastIndex = 0;
|
|
1924
|
-
if (CARD_PATTERN.test(out)) {
|
|
1925
|
-
out = out.replace(CARD_PATTERN, REPLACEMENT_CARD);
|
|
1926
|
-
}
|
|
1927
|
-
CARD_PATTERN.lastIndex = 0;
|
|
1928
|
-
return out;
|
|
2020
|
+
return value.replace(EMAIL_PATTERN, REPLACEMENT_EMAIL).replace(CARD_PATTERN, REPLACEMENT_CARD);
|
|
1929
2021
|
}
|
|
1930
2022
|
function scrubPiiFromProperties(properties) {
|
|
1931
2023
|
const out = {};
|
|
1932
2024
|
for (const k of Object.keys(properties)) {
|
|
1933
|
-
|
|
1934
|
-
if (typeof v === "string") {
|
|
1935
|
-
out[k] = scrubPii(v);
|
|
1936
|
-
} else if (Array.isArray(v)) {
|
|
1937
|
-
out[k] = v.map((item) => typeof item === "string" ? scrubPii(item) : item);
|
|
1938
|
-
} else {
|
|
1939
|
-
out[k] = v;
|
|
1940
|
-
}
|
|
2025
|
+
out[k] = scrubValue(properties[k]);
|
|
1941
2026
|
}
|
|
1942
2027
|
return out;
|
|
1943
2028
|
}
|
|
2029
|
+
function scrubValue(v) {
|
|
2030
|
+
if (typeof v === "string") return scrubPii(v);
|
|
2031
|
+
if (Array.isArray(v)) return v.map(scrubValue);
|
|
2032
|
+
if (v && typeof v === "object" && v.constructor === Object) {
|
|
2033
|
+
return scrubPiiFromProperties(v);
|
|
2034
|
+
}
|
|
2035
|
+
return v;
|
|
2036
|
+
}
|
|
1944
2037
|
|
|
1945
2038
|
// src/breadcrumbs.ts
|
|
1946
2039
|
var BreadcrumbBuffer = class {
|
|
@@ -2230,16 +2323,18 @@ var ErrorTracker = class {
|
|
|
2230
2323
|
const url = typeof input === "string" ? input : input?.url ?? "";
|
|
2231
2324
|
const method = (init.method || "GET").toUpperCase();
|
|
2232
2325
|
const start = Date.now();
|
|
2233
|
-
this.opts.
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2326
|
+
if (!isSelfRequest(url, this.opts.selfHostname)) {
|
|
2327
|
+
this.opts.breadcrumbs.add({
|
|
2328
|
+
timestamp: start,
|
|
2329
|
+
category: "http",
|
|
2330
|
+
message: `${method} ${url}`,
|
|
2331
|
+
data: { url, method }
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2239
2334
|
try {
|
|
2240
2335
|
const response = await origFetch(...args);
|
|
2241
2336
|
if (response.status >= 500 && this.opts.isConsented()) {
|
|
2242
|
-
if (!url.
|
|
2337
|
+
if (!isSelfRequest(url, this.opts.selfHostname)) {
|
|
2243
2338
|
this.captureHttp({
|
|
2244
2339
|
url,
|
|
2245
2340
|
method,
|
|
@@ -2288,7 +2383,7 @@ var ErrorTracker = class {
|
|
|
2288
2383
|
try {
|
|
2289
2384
|
if (xhr.status >= 500 && tracker.opts.isConsented()) {
|
|
2290
2385
|
const url = xhr._cdUrl ?? "";
|
|
2291
|
-
if (!url.
|
|
2386
|
+
if (!isSelfRequest(url, tracker.opts.selfHostname)) {
|
|
2292
2387
|
tracker.captureHttp({
|
|
2293
2388
|
url,
|
|
2294
2389
|
method: (xhr._cdMethod ?? "GET").toUpperCase(),
|
|
@@ -2448,9 +2543,10 @@ var ErrorTracker = class {
|
|
|
2448
2543
|
if (!this.passesSample(err)) return;
|
|
2449
2544
|
if (!this.passesRateLimit(err)) return;
|
|
2450
2545
|
let finalErr = err;
|
|
2451
|
-
|
|
2546
|
+
const hook = this.opts.beforeSend?.();
|
|
2547
|
+
if (hook) {
|
|
2452
2548
|
try {
|
|
2453
|
-
finalErr =
|
|
2549
|
+
finalErr = hook(err);
|
|
2454
2550
|
} catch {
|
|
2455
2551
|
finalErr = err;
|
|
2456
2552
|
}
|
|
@@ -2632,6 +2728,22 @@ function safeClone(v) {
|
|
|
2632
2728
|
function safeStringify2(v) {
|
|
2633
2729
|
return coerceErrorPayload(v).message;
|
|
2634
2730
|
}
|
|
2731
|
+
function extractSelfHostname(baseUrl) {
|
|
2732
|
+
if (!baseUrl || typeof baseUrl !== "string") return null;
|
|
2733
|
+
try {
|
|
2734
|
+
return new URL(baseUrl).hostname.toLowerCase();
|
|
2735
|
+
} catch {
|
|
2736
|
+
return null;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
function isSelfRequest(requestUrl, selfHostname) {
|
|
2740
|
+
if (!selfHostname || !requestUrl) return false;
|
|
2741
|
+
try {
|
|
2742
|
+
return new URL(requestUrl).hostname.toLowerCase() === selfHostname;
|
|
2743
|
+
} catch {
|
|
2744
|
+
return false;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2635
2747
|
|
|
2636
2748
|
// src/crossdeck.ts
|
|
2637
2749
|
var CrossdeckClient = class {
|
|
@@ -2648,6 +2760,24 @@ var CrossdeckClient = class {
|
|
|
2648
2760
|
* mismatched env fails fast at boot rather than at first event-flush.
|
|
2649
2761
|
*/
|
|
2650
2762
|
init(options) {
|
|
2763
|
+
if (this.state) {
|
|
2764
|
+
try {
|
|
2765
|
+
this.state.uninstallUnloadFlush?.();
|
|
2766
|
+
} catch {
|
|
2767
|
+
}
|
|
2768
|
+
try {
|
|
2769
|
+
this.state.autoTracker?.uninstall();
|
|
2770
|
+
} catch {
|
|
2771
|
+
}
|
|
2772
|
+
try {
|
|
2773
|
+
this.state.webVitals?.uninstall();
|
|
2774
|
+
} catch {
|
|
2775
|
+
}
|
|
2776
|
+
try {
|
|
2777
|
+
this.state.errors?.uninstall();
|
|
2778
|
+
} catch {
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2651
2781
|
if (!options.publicKey || !options.publicKey.startsWith("cd_pub_")) {
|
|
2652
2782
|
throw new CrossdeckError({
|
|
2653
2783
|
type: "configuration_error",
|
|
@@ -2755,6 +2885,15 @@ var CrossdeckClient = class {
|
|
|
2755
2885
|
`Event flush failed (${info.lastError}). Retrying in ${info.delayMs}ms (attempt ${info.consecutiveFailures}).`,
|
|
2756
2886
|
{ ...info }
|
|
2757
2887
|
);
|
|
2888
|
+
},
|
|
2889
|
+
onPermanentFailure: (info) => {
|
|
2890
|
+
const headline = `[crossdeck] Event batch DROPPED (status ${info.status}): ${info.lastError}. ${info.droppedCount} event(s) lost \u2014 check your publishable key + app config.`;
|
|
2891
|
+
console.error(headline);
|
|
2892
|
+
debug.emit(
|
|
2893
|
+
"sdk.flush_permanent_failure",
|
|
2894
|
+
headline,
|
|
2895
|
+
{ ...info }
|
|
2896
|
+
);
|
|
2758
2897
|
}
|
|
2759
2898
|
});
|
|
2760
2899
|
const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
|
|
@@ -2821,8 +2960,20 @@ var CrossdeckClient = class {
|
|
|
2821
2960
|
report: (err) => this.reportError(err),
|
|
2822
2961
|
getContext: () => ({ ...this.state.errorContext }),
|
|
2823
2962
|
getTags: () => ({ ...this.state.errorTags }),
|
|
2824
|
-
|
|
2825
|
-
|
|
2963
|
+
// GETTER, not a captured value — `setErrorBeforeSend()` mutates
|
|
2964
|
+
// `state.errorBeforeSend` after init() and the tracker MUST
|
|
2965
|
+
// pick up the new hook on the next error. The pre-fix shape
|
|
2966
|
+
// (`beforeSend: this.state!.errorBeforeSend`) snapshotted
|
|
2967
|
+
// `null` at construction and made the customer's PII scrubber
|
|
2968
|
+
// silently inert. See error-capture.ts:ErrorTrackerOptions.beforeSend.
|
|
2969
|
+
beforeSend: () => this.state.errorBeforeSend,
|
|
2970
|
+
isConsented: () => this.state.consent.errors,
|
|
2971
|
+
// Derived from the configured baseUrl at init() time. Used by
|
|
2972
|
+
// the fetch / XHR wrappers to skip captureHttp on Crossdeck's
|
|
2973
|
+
// own requests — pre-fix the skip was hardcoded to
|
|
2974
|
+
// `api.cross-deck.com` and broke for customers on staging /
|
|
2975
|
+
// regional / self-hosted base URLs (recursive capture loop).
|
|
2976
|
+
selfHostname: extractSelfHostname(opts.baseUrl)
|
|
2826
2977
|
});
|
|
2827
2978
|
this.state.errors = tracker;
|
|
2828
2979
|
tracker.install();
|
|
@@ -2905,7 +3056,8 @@ var CrossdeckClient = class {
|
|
|
2905
3056
|
body
|
|
2906
3057
|
});
|
|
2907
3058
|
const priorCdcust = s.identity.crossdeckCustomerId;
|
|
2908
|
-
|
|
3059
|
+
const cacheHasEntries = s.entitlements.list().length > 0;
|
|
3060
|
+
if (priorCdcust && result.crossdeckCustomerId && priorCdcust !== result.crossdeckCustomerId || !priorCdcust && cacheHasEntries) {
|
|
2909
3061
|
s.entitlements.clear();
|
|
2910
3062
|
}
|
|
2911
3063
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
@@ -3347,8 +3499,9 @@ var CrossdeckClient = class {
|
|
|
3347
3499
|
message: "syncPurchases requires a signedTransactionInfo string from StoreKit 2."
|
|
3348
3500
|
});
|
|
3349
3501
|
}
|
|
3502
|
+
const rail = input.rail ?? "apple";
|
|
3350
3503
|
const result = await s.http.request("POST", "/purchases/sync", {
|
|
3351
|
-
body: {
|
|
3504
|
+
body: { ...input, rail }
|
|
3352
3505
|
});
|
|
3353
3506
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
3354
3507
|
s.entitlements.setFromList(result.entitlements);
|
|
@@ -3414,6 +3567,8 @@ var CrossdeckClient = class {
|
|
|
3414
3567
|
this.state.errorContext = {};
|
|
3415
3568
|
this.state.errorTags = {};
|
|
3416
3569
|
this.state.developerUserId = null;
|
|
3570
|
+
this.state.lastServerTime = null;
|
|
3571
|
+
this.state.lastClientTime = null;
|
|
3417
3572
|
if (this.state.autoTracker) {
|
|
3418
3573
|
const tracker = new AutoTracker(
|
|
3419
3574
|
this.state.options.autoTrack,
|
|
@@ -3542,7 +3697,9 @@ function isLocalHostname() {
|
|
|
3542
3697
|
const hostname = w?.location?.hostname;
|
|
3543
3698
|
if (!hostname) return false;
|
|
3544
3699
|
if (hostname === "localhost" || hostname === "127.0.0.1") return true;
|
|
3700
|
+
if (hostname === "0.0.0.0") return true;
|
|
3545
3701
|
if (hostname === "::1" || hostname === "[::1]") return true;
|
|
3702
|
+
if (/^\[?fe80::/i.test(hostname)) return true;
|
|
3546
3703
|
if (hostname.endsWith(".local")) return true;
|
|
3547
3704
|
if (/^10\./.test(hostname)) return true;
|
|
3548
3705
|
if (/^192\.168\./.test(hostname)) return true;
|