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