@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/error-codes.json
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -97,9 +97,11 @@ function typeMapForStatus(status) {
|
|
|
97
97
|
return "internal_error";
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
// src/
|
|
100
|
+
// src/_version.ts
|
|
101
|
+
var SDK_VERSION = "1.3.0";
|
|
101
102
|
var SDK_NAME = "@cross-deck/web";
|
|
102
|
-
|
|
103
|
+
|
|
104
|
+
// src/http.ts
|
|
103
105
|
var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
104
106
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
105
107
|
var HttpClient = class {
|
|
@@ -530,8 +532,10 @@ function computeNextDelay(attempts, retryAfterMs, options = {}, random = Math.ra
|
|
|
530
532
|
const safeAttempts = Math.min(attempts, 30);
|
|
531
533
|
const ceiling = Math.min(max, base * Math.pow(factor, safeAttempts));
|
|
532
534
|
const jittered = ceiling * random();
|
|
533
|
-
if (retryAfterMs !== void 0
|
|
534
|
-
|
|
535
|
+
if (retryAfterMs !== void 0) {
|
|
536
|
+
const ABSOLUTE_MAX_MS = 24 * 60 * 60 * 1e3;
|
|
537
|
+
const honoured = Math.min(ABSOLUTE_MAX_MS, retryAfterMs);
|
|
538
|
+
if (honoured > jittered) return honoured;
|
|
535
539
|
}
|
|
536
540
|
return Math.max(0, Math.round(jittered));
|
|
537
541
|
}
|
|
@@ -566,6 +570,27 @@ var EventQueue = class {
|
|
|
566
570
|
constructor(cfg) {
|
|
567
571
|
this.cfg = cfg;
|
|
568
572
|
this.buffer = [];
|
|
573
|
+
/**
|
|
574
|
+
* In-flight events that have been spliced from `buffer` for the
|
|
575
|
+
* current batch but haven't yet been confirmed (success or permanent
|
|
576
|
+
* failure). On a retry-driven re-flush we re-use this slot alongside
|
|
577
|
+
* `pendingBatchId` so the Stripe-style Idempotency-Key is preserved
|
|
578
|
+
* across retries of the SAME logical batch.
|
|
579
|
+
*
|
|
580
|
+
* Pre-fix the splice cleared the buffer AND we immediately
|
|
581
|
+
* `persistent.save(empty)` BEFORE awaiting the network call — a
|
|
582
|
+
* crash mid-flight wiped the persisted blob and the batch was lost
|
|
583
|
+
* with no replay on next boot. Now the persisted blob always carries
|
|
584
|
+
* `[...pendingBatch, ...buffer]` so the in-flight events survive
|
|
585
|
+
* until the server confirms them.
|
|
586
|
+
*
|
|
587
|
+
* Pre-fix every retry attempt also minted a NEW batchId via
|
|
588
|
+
* `splice + mintBatchId`, defeating the backend's
|
|
589
|
+
* `Idempotency-Key` dedup. Reuse via this slot brings web in
|
|
590
|
+
* lockstep with node (which already had the field).
|
|
591
|
+
*/
|
|
592
|
+
this.pendingBatch = null;
|
|
593
|
+
this.pendingBatchId = null;
|
|
569
594
|
this.dropped = 0;
|
|
570
595
|
this.inFlight = 0;
|
|
571
596
|
this.lastFlushAt = 0;
|
|
@@ -598,7 +623,7 @@ var EventQueue = class {
|
|
|
598
623
|
this.cfg.onDrop?.(overflow);
|
|
599
624
|
}
|
|
600
625
|
this.cfg.onBufferChange?.(this.buffer.length);
|
|
601
|
-
this.
|
|
626
|
+
this.persistAll();
|
|
602
627
|
if (this.buffer.length >= this.cfg.batchSize) {
|
|
603
628
|
void this.flush();
|
|
604
629
|
} else {
|
|
@@ -607,22 +632,44 @@ var EventQueue = class {
|
|
|
607
632
|
}
|
|
608
633
|
/**
|
|
609
634
|
* Flush the buffer to /v1/events. Resolves when the network call
|
|
610
|
-
* completes (success or failure). On
|
|
611
|
-
*
|
|
635
|
+
* completes (success or failure). On a retryable failure the batch
|
|
636
|
+
* stays in `pendingBatch` for the next scheduled retry — the SAME
|
|
637
|
+
* batch with the SAME `Idempotency-Key` is re-sent (Stripe pattern).
|
|
638
|
+
*
|
|
639
|
+
* Three terminal states from one call:
|
|
640
|
+
* - 2xx success: pendingBatch cleared, persisted state collapses to
|
|
641
|
+
* just `buffer` (any new events that arrived during in-flight).
|
|
642
|
+
* - 4xx permanent (except 408/429): pendingBatch DROPPED, `dropped`
|
|
643
|
+
* counter increments, a `permanent_failure` signal fires. The
|
|
644
|
+
* server is telling us our request is malformed / our key is
|
|
645
|
+
* revoked / we lack permission — retrying with the same key
|
|
646
|
+
* forever just grows the queue while the customer thinks events
|
|
647
|
+
* are landing.
|
|
648
|
+
* - 5xx / network / 408 / 429: pendingBatch + batchId stay; backoff
|
|
649
|
+
* schedules a retry; the next `flush()` re-uses both.
|
|
612
650
|
*
|
|
613
651
|
* `options.keepalive` marks the underlying fetch as keepalive so the
|
|
614
|
-
* browser keeps the request alive past page unload. Use
|
|
615
|
-
*
|
|
652
|
+
* browser keeps the request alive past page unload. Use for terminal
|
|
653
|
+
* flushes (pagehide / visibilitychange→hidden / beforeunload).
|
|
616
654
|
*/
|
|
617
655
|
async flush(options = {}) {
|
|
618
|
-
|
|
656
|
+
let batch;
|
|
657
|
+
let batchId;
|
|
658
|
+
if (this.pendingBatch !== null && this.pendingBatchId !== null) {
|
|
659
|
+
batch = this.pendingBatch;
|
|
660
|
+
batchId = this.pendingBatchId;
|
|
661
|
+
} else {
|
|
662
|
+
if (this.buffer.length === 0) return null;
|
|
663
|
+
batch = this.buffer.splice(0);
|
|
664
|
+
batchId = this.mintBatchId();
|
|
665
|
+
this.pendingBatch = batch;
|
|
666
|
+
this.pendingBatchId = batchId;
|
|
667
|
+
this.inFlight += batch.length;
|
|
668
|
+
this.cfg.onBufferChange?.(this.buffer.length);
|
|
669
|
+
this.persistAll();
|
|
670
|
+
}
|
|
619
671
|
this.cancelTimerIfSet();
|
|
620
672
|
this.nextRetryAt = null;
|
|
621
|
-
const batch = this.buffer.splice(0);
|
|
622
|
-
const batchId = this.mintBatchId();
|
|
623
|
-
this.inFlight += batch.length;
|
|
624
|
-
this.persistent?.save(this.buffer);
|
|
625
|
-
this.cfg.onBufferChange?.(this.buffer.length);
|
|
626
673
|
try {
|
|
627
674
|
const env = this.cfg.envelope();
|
|
628
675
|
const result = await this.cfg.http.request("POST", "/events", {
|
|
@@ -641,20 +688,33 @@ var EventQueue = class {
|
|
|
641
688
|
this.lastFlushAt = Date.now();
|
|
642
689
|
this.lastError = null;
|
|
643
690
|
this.inFlight -= batch.length;
|
|
691
|
+
this.pendingBatch = null;
|
|
692
|
+
this.pendingBatchId = null;
|
|
644
693
|
this.retry.recordSuccess();
|
|
645
|
-
this.
|
|
694
|
+
this.persistAll();
|
|
646
695
|
if (!this.firstFlushFired) {
|
|
647
696
|
this.firstFlushFired = true;
|
|
648
697
|
this.cfg.onFirstFlushSuccess?.();
|
|
649
698
|
}
|
|
650
699
|
return result;
|
|
651
700
|
} catch (err) {
|
|
652
|
-
this.buffer.unshift(...batch);
|
|
653
|
-
this.inFlight -= batch.length;
|
|
654
701
|
const message = err instanceof Error ? err.message : String(err);
|
|
655
702
|
this.lastError = message;
|
|
656
|
-
|
|
657
|
-
|
|
703
|
+
if (isPermanent4xx(err)) {
|
|
704
|
+
const droppedCount = batch.length;
|
|
705
|
+
this.pendingBatch = null;
|
|
706
|
+
this.pendingBatchId = null;
|
|
707
|
+
this.inFlight -= droppedCount;
|
|
708
|
+
this.dropped += droppedCount;
|
|
709
|
+
this.persistAll();
|
|
710
|
+
this.cfg.onDrop?.(droppedCount);
|
|
711
|
+
this.cfg.onPermanentFailure?.({
|
|
712
|
+
status: err.status ?? 0,
|
|
713
|
+
droppedCount,
|
|
714
|
+
lastError: message
|
|
715
|
+
});
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
658
718
|
const retryAfterMs = extractRetryAfterMs(err);
|
|
659
719
|
const delay = this.retry.nextDelay(retryAfterMs);
|
|
660
720
|
this.scheduleRetry(delay);
|
|
@@ -672,6 +732,8 @@ var EventQueue = class {
|
|
|
672
732
|
this.cancelTimerIfSet();
|
|
673
733
|
this.nextRetryAt = null;
|
|
674
734
|
this.buffer = [];
|
|
735
|
+
this.pendingBatch = null;
|
|
736
|
+
this.pendingBatchId = null;
|
|
675
737
|
this.dropped = 0;
|
|
676
738
|
this.inFlight = 0;
|
|
677
739
|
this.lastError = null;
|
|
@@ -681,6 +743,10 @@ var EventQueue = class {
|
|
|
681
743
|
}
|
|
682
744
|
getStats() {
|
|
683
745
|
return {
|
|
746
|
+
// `buffered` counts events waiting for their FIRST flush. The
|
|
747
|
+
// in-flight pendingBatch (retrying) is tracked separately via
|
|
748
|
+
// `inFlight` — surfacing both lets diagnostics show "we have
|
|
749
|
+
// events stuck retrying" distinct from "new events arriving".
|
|
684
750
|
buffered: this.buffer.length,
|
|
685
751
|
dropped: this.dropped,
|
|
686
752
|
inFlight: this.inFlight,
|
|
@@ -690,6 +756,29 @@ var EventQueue = class {
|
|
|
690
756
|
nextRetryAt: this.nextRetryAt
|
|
691
757
|
};
|
|
692
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* The Idempotency-Key of the in-flight pending batch (if any).
|
|
761
|
+
* Exposed for testing the Stripe-style retry-reuse contract.
|
|
762
|
+
* Production callers don't need this.
|
|
763
|
+
*/
|
|
764
|
+
get pendingIdempotencyKey() {
|
|
765
|
+
return this.pendingBatchId;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Persist `[...pendingBatch, ...buffer]` — the full set of
|
|
769
|
+
* not-yet-confirmed events. The next boot rehydrates this exact set
|
|
770
|
+
* into `buffer` and replays. The server dedups via eventId
|
|
771
|
+
* (ReplacingMergeTree on the warehouse side), so re-sending an event
|
|
772
|
+
* that may have already landed is safe.
|
|
773
|
+
*/
|
|
774
|
+
persistAll() {
|
|
775
|
+
if (!this.persistent) return;
|
|
776
|
+
if (this.pendingBatch === null) {
|
|
777
|
+
this.persistent.save(this.buffer);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
this.persistent.save([...this.pendingBatch, ...this.buffer]);
|
|
781
|
+
}
|
|
693
782
|
// ---------- internal scheduling ----------
|
|
694
783
|
scheduleIdleFlush() {
|
|
695
784
|
this.cancelTimerIfSet();
|
|
@@ -723,6 +812,14 @@ function extractRetryAfterMs(err) {
|
|
|
723
812
|
}
|
|
724
813
|
return void 0;
|
|
725
814
|
}
|
|
815
|
+
function isPermanent4xx(err) {
|
|
816
|
+
if (!err || typeof err !== "object") return false;
|
|
817
|
+
const status = err.status;
|
|
818
|
+
if (typeof status !== "number" || !Number.isFinite(status)) return false;
|
|
819
|
+
if (status < 400 || status >= 500) return false;
|
|
820
|
+
if (status === 408 || status === 429) return false;
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
726
823
|
function defaultScheduler(fn, ms) {
|
|
727
824
|
const id = setTimeout(fn, ms);
|
|
728
825
|
if (typeof id.unref === "function") {
|
|
@@ -1064,6 +1161,7 @@ var AutoTracker = class {
|
|
|
1064
1161
|
/** Exposed for tests + consumers that want to reset the session manually. */
|
|
1065
1162
|
resetSession() {
|
|
1066
1163
|
if (this.session && !this.session.endedSent) this.emitSessionEnd();
|
|
1164
|
+
this.pageviewId = null;
|
|
1067
1165
|
this.session = this.startNewSession();
|
|
1068
1166
|
this.emitSessionStart();
|
|
1069
1167
|
}
|
|
@@ -1100,6 +1198,7 @@ var AutoTracker = class {
|
|
|
1100
1198
|
const hiddenFor = this.session.hiddenAt ? Date.now() - this.session.hiddenAt : 0;
|
|
1101
1199
|
if (hiddenFor >= SESSION_RESUME_THRESHOLD_MS) {
|
|
1102
1200
|
this.emitSessionEnd();
|
|
1201
|
+
this.pageviewId = null;
|
|
1103
1202
|
this.session = this.startNewSession();
|
|
1104
1203
|
this.emitSessionStart();
|
|
1105
1204
|
} else {
|
|
@@ -1473,7 +1572,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1473
1572
|
const maxStringLength = options.maxStringLength ?? DEFAULT_MAX_STRING;
|
|
1474
1573
|
const maxBatchPropertyBytes = options.maxBatchPropertyBytes ?? DEFAULT_MAX_BYTES;
|
|
1475
1574
|
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
1476
|
-
const seen = /* @__PURE__ */ new
|
|
1575
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1477
1576
|
const visit = (value, key, depth) => {
|
|
1478
1577
|
if (depth > maxDepth) {
|
|
1479
1578
|
warnings.push({ kind: "depth_exceeded", key });
|
|
@@ -1561,6 +1660,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1561
1660
|
const result = visit(value[i], `${key}[${i}]`, depth + 1);
|
|
1562
1661
|
if (result.keep) out.push(result.value);
|
|
1563
1662
|
}
|
|
1663
|
+
seen.delete(value);
|
|
1564
1664
|
return { keep: true, value: out };
|
|
1565
1665
|
}
|
|
1566
1666
|
if (t === "object") {
|
|
@@ -1575,6 +1675,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1575
1675
|
const result = visit(obj[k], `${key}.${k}`, depth + 1);
|
|
1576
1676
|
if (result.keep) out[k] = result.value;
|
|
1577
1677
|
}
|
|
1678
|
+
seen.delete(obj);
|
|
1578
1679
|
return { keep: true, value: out };
|
|
1579
1680
|
}
|
|
1580
1681
|
warnings.push({ kind: "non_serialisable", key });
|
|
@@ -1917,35 +2018,27 @@ var ConsentManager = class {
|
|
|
1917
2018
|
};
|
|
1918
2019
|
var EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
1919
2020
|
var CARD_PATTERN = /\b\d(?:[ -]?\d){12,18}\b/g;
|
|
1920
|
-
var REPLACEMENT_EMAIL = "
|
|
1921
|
-
var REPLACEMENT_CARD = "
|
|
2021
|
+
var REPLACEMENT_EMAIL = "<email>";
|
|
2022
|
+
var REPLACEMENT_CARD = "<card>";
|
|
1922
2023
|
function scrubPii(value) {
|
|
1923
2024
|
if (!value) return value;
|
|
1924
|
-
|
|
1925
|
-
if (EMAIL_PATTERN.test(out)) {
|
|
1926
|
-
out = out.replace(EMAIL_PATTERN, REPLACEMENT_EMAIL);
|
|
1927
|
-
}
|
|
1928
|
-
EMAIL_PATTERN.lastIndex = 0;
|
|
1929
|
-
if (CARD_PATTERN.test(out)) {
|
|
1930
|
-
out = out.replace(CARD_PATTERN, REPLACEMENT_CARD);
|
|
1931
|
-
}
|
|
1932
|
-
CARD_PATTERN.lastIndex = 0;
|
|
1933
|
-
return out;
|
|
2025
|
+
return value.replace(EMAIL_PATTERN, REPLACEMENT_EMAIL).replace(CARD_PATTERN, REPLACEMENT_CARD);
|
|
1934
2026
|
}
|
|
1935
2027
|
function scrubPiiFromProperties(properties) {
|
|
1936
2028
|
const out = {};
|
|
1937
2029
|
for (const k of Object.keys(properties)) {
|
|
1938
|
-
|
|
1939
|
-
if (typeof v === "string") {
|
|
1940
|
-
out[k] = scrubPii(v);
|
|
1941
|
-
} else if (Array.isArray(v)) {
|
|
1942
|
-
out[k] = v.map((item) => typeof item === "string" ? scrubPii(item) : item);
|
|
1943
|
-
} else {
|
|
1944
|
-
out[k] = v;
|
|
1945
|
-
}
|
|
2030
|
+
out[k] = scrubValue(properties[k]);
|
|
1946
2031
|
}
|
|
1947
2032
|
return out;
|
|
1948
2033
|
}
|
|
2034
|
+
function scrubValue(v) {
|
|
2035
|
+
if (typeof v === "string") return scrubPii(v);
|
|
2036
|
+
if (Array.isArray(v)) return v.map(scrubValue);
|
|
2037
|
+
if (v && typeof v === "object" && v.constructor === Object) {
|
|
2038
|
+
return scrubPiiFromProperties(v);
|
|
2039
|
+
}
|
|
2040
|
+
return v;
|
|
2041
|
+
}
|
|
1949
2042
|
|
|
1950
2043
|
// src/breadcrumbs.ts
|
|
1951
2044
|
var BreadcrumbBuffer = class {
|
|
@@ -2235,16 +2328,18 @@ var ErrorTracker = class {
|
|
|
2235
2328
|
const url = typeof input === "string" ? input : input?.url ?? "";
|
|
2236
2329
|
const method = (init.method || "GET").toUpperCase();
|
|
2237
2330
|
const start = Date.now();
|
|
2238
|
-
this.opts.
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2331
|
+
if (!isSelfRequest(url, this.opts.selfHostname)) {
|
|
2332
|
+
this.opts.breadcrumbs.add({
|
|
2333
|
+
timestamp: start,
|
|
2334
|
+
category: "http",
|
|
2335
|
+
message: `${method} ${url}`,
|
|
2336
|
+
data: { url, method }
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2244
2339
|
try {
|
|
2245
2340
|
const response = await origFetch(...args);
|
|
2246
2341
|
if (response.status >= 500 && this.opts.isConsented()) {
|
|
2247
|
-
if (!url.
|
|
2342
|
+
if (!isSelfRequest(url, this.opts.selfHostname)) {
|
|
2248
2343
|
this.captureHttp({
|
|
2249
2344
|
url,
|
|
2250
2345
|
method,
|
|
@@ -2293,7 +2388,7 @@ var ErrorTracker = class {
|
|
|
2293
2388
|
try {
|
|
2294
2389
|
if (xhr.status >= 500 && tracker.opts.isConsented()) {
|
|
2295
2390
|
const url = xhr._cdUrl ?? "";
|
|
2296
|
-
if (!url.
|
|
2391
|
+
if (!isSelfRequest(url, tracker.opts.selfHostname)) {
|
|
2297
2392
|
tracker.captureHttp({
|
|
2298
2393
|
url,
|
|
2299
2394
|
method: (xhr._cdMethod ?? "GET").toUpperCase(),
|
|
@@ -2453,9 +2548,10 @@ var ErrorTracker = class {
|
|
|
2453
2548
|
if (!this.passesSample(err)) return;
|
|
2454
2549
|
if (!this.passesRateLimit(err)) return;
|
|
2455
2550
|
let finalErr = err;
|
|
2456
|
-
|
|
2551
|
+
const hook = this.opts.beforeSend?.();
|
|
2552
|
+
if (hook) {
|
|
2457
2553
|
try {
|
|
2458
|
-
finalErr =
|
|
2554
|
+
finalErr = hook(err);
|
|
2459
2555
|
} catch {
|
|
2460
2556
|
finalErr = err;
|
|
2461
2557
|
}
|
|
@@ -2637,6 +2733,22 @@ function safeClone(v) {
|
|
|
2637
2733
|
function safeStringify2(v) {
|
|
2638
2734
|
return coerceErrorPayload(v).message;
|
|
2639
2735
|
}
|
|
2736
|
+
function extractSelfHostname(baseUrl) {
|
|
2737
|
+
if (!baseUrl || typeof baseUrl !== "string") return null;
|
|
2738
|
+
try {
|
|
2739
|
+
return new URL(baseUrl).hostname.toLowerCase();
|
|
2740
|
+
} catch {
|
|
2741
|
+
return null;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
function isSelfRequest(requestUrl, selfHostname) {
|
|
2745
|
+
if (!selfHostname || !requestUrl) return false;
|
|
2746
|
+
try {
|
|
2747
|
+
return new URL(requestUrl).hostname.toLowerCase() === selfHostname;
|
|
2748
|
+
} catch {
|
|
2749
|
+
return false;
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2640
2752
|
|
|
2641
2753
|
// src/crossdeck.ts
|
|
2642
2754
|
var CrossdeckClient = class {
|
|
@@ -2653,6 +2765,24 @@ var CrossdeckClient = class {
|
|
|
2653
2765
|
* mismatched env fails fast at boot rather than at first event-flush.
|
|
2654
2766
|
*/
|
|
2655
2767
|
init(options) {
|
|
2768
|
+
if (this.state) {
|
|
2769
|
+
try {
|
|
2770
|
+
this.state.uninstallUnloadFlush?.();
|
|
2771
|
+
} catch {
|
|
2772
|
+
}
|
|
2773
|
+
try {
|
|
2774
|
+
this.state.autoTracker?.uninstall();
|
|
2775
|
+
} catch {
|
|
2776
|
+
}
|
|
2777
|
+
try {
|
|
2778
|
+
this.state.webVitals?.uninstall();
|
|
2779
|
+
} catch {
|
|
2780
|
+
}
|
|
2781
|
+
try {
|
|
2782
|
+
this.state.errors?.uninstall();
|
|
2783
|
+
} catch {
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2656
2786
|
if (!options.publicKey || !options.publicKey.startsWith("cd_pub_")) {
|
|
2657
2787
|
throw new CrossdeckError({
|
|
2658
2788
|
type: "configuration_error",
|
|
@@ -2760,6 +2890,15 @@ var CrossdeckClient = class {
|
|
|
2760
2890
|
`Event flush failed (${info.lastError}). Retrying in ${info.delayMs}ms (attempt ${info.consecutiveFailures}).`,
|
|
2761
2891
|
{ ...info }
|
|
2762
2892
|
);
|
|
2893
|
+
},
|
|
2894
|
+
onPermanentFailure: (info) => {
|
|
2895
|
+
const headline = `[crossdeck] Event batch DROPPED (status ${info.status}): ${info.lastError}. ${info.droppedCount} event(s) lost \u2014 check your publishable key + app config.`;
|
|
2896
|
+
console.error(headline);
|
|
2897
|
+
debug.emit(
|
|
2898
|
+
"sdk.flush_permanent_failure",
|
|
2899
|
+
headline,
|
|
2900
|
+
{ ...info }
|
|
2901
|
+
);
|
|
2763
2902
|
}
|
|
2764
2903
|
});
|
|
2765
2904
|
const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
|
|
@@ -2826,8 +2965,20 @@ var CrossdeckClient = class {
|
|
|
2826
2965
|
report: (err) => this.reportError(err),
|
|
2827
2966
|
getContext: () => ({ ...this.state.errorContext }),
|
|
2828
2967
|
getTags: () => ({ ...this.state.errorTags }),
|
|
2829
|
-
|
|
2830
|
-
|
|
2968
|
+
// GETTER, not a captured value — `setErrorBeforeSend()` mutates
|
|
2969
|
+
// `state.errorBeforeSend` after init() and the tracker MUST
|
|
2970
|
+
// pick up the new hook on the next error. The pre-fix shape
|
|
2971
|
+
// (`beforeSend: this.state!.errorBeforeSend`) snapshotted
|
|
2972
|
+
// `null` at construction and made the customer's PII scrubber
|
|
2973
|
+
// silently inert. See error-capture.ts:ErrorTrackerOptions.beforeSend.
|
|
2974
|
+
beforeSend: () => this.state.errorBeforeSend,
|
|
2975
|
+
isConsented: () => this.state.consent.errors,
|
|
2976
|
+
// Derived from the configured baseUrl at init() time. Used by
|
|
2977
|
+
// the fetch / XHR wrappers to skip captureHttp on Crossdeck's
|
|
2978
|
+
// own requests — pre-fix the skip was hardcoded to
|
|
2979
|
+
// `api.cross-deck.com` and broke for customers on staging /
|
|
2980
|
+
// regional / self-hosted base URLs (recursive capture loop).
|
|
2981
|
+
selfHostname: extractSelfHostname(opts.baseUrl)
|
|
2831
2982
|
});
|
|
2832
2983
|
this.state.errors = tracker;
|
|
2833
2984
|
tracker.install();
|
|
@@ -2910,7 +3061,8 @@ var CrossdeckClient = class {
|
|
|
2910
3061
|
body
|
|
2911
3062
|
});
|
|
2912
3063
|
const priorCdcust = s.identity.crossdeckCustomerId;
|
|
2913
|
-
|
|
3064
|
+
const cacheHasEntries = s.entitlements.list().length > 0;
|
|
3065
|
+
if (priorCdcust && result.crossdeckCustomerId && priorCdcust !== result.crossdeckCustomerId || !priorCdcust && cacheHasEntries) {
|
|
2914
3066
|
s.entitlements.clear();
|
|
2915
3067
|
}
|
|
2916
3068
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
@@ -3352,8 +3504,9 @@ var CrossdeckClient = class {
|
|
|
3352
3504
|
message: "syncPurchases requires a signedTransactionInfo string from StoreKit 2."
|
|
3353
3505
|
});
|
|
3354
3506
|
}
|
|
3507
|
+
const rail = input.rail ?? "apple";
|
|
3355
3508
|
const result = await s.http.request("POST", "/purchases/sync", {
|
|
3356
|
-
body: {
|
|
3509
|
+
body: { ...input, rail }
|
|
3357
3510
|
});
|
|
3358
3511
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
3359
3512
|
s.entitlements.setFromList(result.entitlements);
|
|
@@ -3419,6 +3572,8 @@ var CrossdeckClient = class {
|
|
|
3419
3572
|
this.state.errorContext = {};
|
|
3420
3573
|
this.state.errorTags = {};
|
|
3421
3574
|
this.state.developerUserId = null;
|
|
3575
|
+
this.state.lastServerTime = null;
|
|
3576
|
+
this.state.lastClientTime = null;
|
|
3422
3577
|
if (this.state.autoTracker) {
|
|
3423
3578
|
const tracker = new AutoTracker(
|
|
3424
3579
|
this.state.options.autoTrack,
|
|
@@ -3547,7 +3702,9 @@ function isLocalHostname() {
|
|
|
3547
3702
|
const hostname = w?.location?.hostname;
|
|
3548
3703
|
if (!hostname) return false;
|
|
3549
3704
|
if (hostname === "localhost" || hostname === "127.0.0.1") return true;
|
|
3705
|
+
if (hostname === "0.0.0.0") return true;
|
|
3550
3706
|
if (hostname === "::1" || hostname === "[::1]") return true;
|
|
3707
|
+
if (/^\[?fe80::/i.test(hostname)) return true;
|
|
3551
3708
|
if (hostname.endsWith(".local")) return true;
|
|
3552
3709
|
if (/^10\./.test(hostname)) return true;
|
|
3553
3710
|
if (/^192\.168\./.test(hostname)) return true;
|