@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/react.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/react.ts
|
|
21
21
|
var react_exports = {};
|
|
22
22
|
__export(react_exports, {
|
|
23
|
+
CrossdeckProvider: () => CrossdeckProvider,
|
|
23
24
|
useEntitlement: () => useEntitlement,
|
|
24
25
|
useEntitlements: () => useEntitlements
|
|
25
26
|
});
|
|
@@ -91,9 +92,11 @@ function typeMapForStatus(status) {
|
|
|
91
92
|
return "internal_error";
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
// src/
|
|
95
|
+
// src/_version.ts
|
|
96
|
+
var SDK_VERSION = "1.3.0";
|
|
95
97
|
var SDK_NAME = "@cross-deck/web";
|
|
96
|
-
|
|
98
|
+
|
|
99
|
+
// src/http.ts
|
|
97
100
|
var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
98
101
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
99
102
|
var HttpClient = class {
|
|
@@ -524,8 +527,10 @@ function computeNextDelay(attempts, retryAfterMs, options = {}, random = Math.ra
|
|
|
524
527
|
const safeAttempts = Math.min(attempts, 30);
|
|
525
528
|
const ceiling = Math.min(max, base * Math.pow(factor, safeAttempts));
|
|
526
529
|
const jittered = ceiling * random();
|
|
527
|
-
if (retryAfterMs !== void 0
|
|
528
|
-
|
|
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;
|
|
529
534
|
}
|
|
530
535
|
return Math.max(0, Math.round(jittered));
|
|
531
536
|
}
|
|
@@ -560,6 +565,27 @@ var EventQueue = class {
|
|
|
560
565
|
constructor(cfg) {
|
|
561
566
|
this.cfg = cfg;
|
|
562
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;
|
|
563
589
|
this.dropped = 0;
|
|
564
590
|
this.inFlight = 0;
|
|
565
591
|
this.lastFlushAt = 0;
|
|
@@ -592,7 +618,7 @@ var EventQueue = class {
|
|
|
592
618
|
this.cfg.onDrop?.(overflow);
|
|
593
619
|
}
|
|
594
620
|
this.cfg.onBufferChange?.(this.buffer.length);
|
|
595
|
-
this.
|
|
621
|
+
this.persistAll();
|
|
596
622
|
if (this.buffer.length >= this.cfg.batchSize) {
|
|
597
623
|
void this.flush();
|
|
598
624
|
} else {
|
|
@@ -601,22 +627,44 @@ var EventQueue = class {
|
|
|
601
627
|
}
|
|
602
628
|
/**
|
|
603
629
|
* Flush the buffer to /v1/events. Resolves when the network call
|
|
604
|
-
* completes (success or failure). On
|
|
605
|
-
*
|
|
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.
|
|
606
645
|
*
|
|
607
646
|
* `options.keepalive` marks the underlying fetch as keepalive so the
|
|
608
|
-
* browser keeps the request alive past page unload. Use
|
|
609
|
-
*
|
|
647
|
+
* browser keeps the request alive past page unload. Use for terminal
|
|
648
|
+
* flushes (pagehide / visibilitychange→hidden / beforeunload).
|
|
610
649
|
*/
|
|
611
650
|
async flush(options = {}) {
|
|
612
|
-
|
|
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
|
+
}
|
|
613
666
|
this.cancelTimerIfSet();
|
|
614
667
|
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
668
|
try {
|
|
621
669
|
const env = this.cfg.envelope();
|
|
622
670
|
const result = await this.cfg.http.request("POST", "/events", {
|
|
@@ -635,20 +683,33 @@ var EventQueue = class {
|
|
|
635
683
|
this.lastFlushAt = Date.now();
|
|
636
684
|
this.lastError = null;
|
|
637
685
|
this.inFlight -= batch.length;
|
|
686
|
+
this.pendingBatch = null;
|
|
687
|
+
this.pendingBatchId = null;
|
|
638
688
|
this.retry.recordSuccess();
|
|
639
|
-
this.
|
|
689
|
+
this.persistAll();
|
|
640
690
|
if (!this.firstFlushFired) {
|
|
641
691
|
this.firstFlushFired = true;
|
|
642
692
|
this.cfg.onFirstFlushSuccess?.();
|
|
643
693
|
}
|
|
644
694
|
return result;
|
|
645
695
|
} catch (err) {
|
|
646
|
-
this.buffer.unshift(...batch);
|
|
647
|
-
this.inFlight -= batch.length;
|
|
648
696
|
const message = err instanceof Error ? err.message : String(err);
|
|
649
697
|
this.lastError = message;
|
|
650
|
-
|
|
651
|
-
|
|
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
|
+
}
|
|
652
713
|
const retryAfterMs = extractRetryAfterMs(err);
|
|
653
714
|
const delay = this.retry.nextDelay(retryAfterMs);
|
|
654
715
|
this.scheduleRetry(delay);
|
|
@@ -666,6 +727,8 @@ var EventQueue = class {
|
|
|
666
727
|
this.cancelTimerIfSet();
|
|
667
728
|
this.nextRetryAt = null;
|
|
668
729
|
this.buffer = [];
|
|
730
|
+
this.pendingBatch = null;
|
|
731
|
+
this.pendingBatchId = null;
|
|
669
732
|
this.dropped = 0;
|
|
670
733
|
this.inFlight = 0;
|
|
671
734
|
this.lastError = null;
|
|
@@ -675,6 +738,10 @@ var EventQueue = class {
|
|
|
675
738
|
}
|
|
676
739
|
getStats() {
|
|
677
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".
|
|
678
745
|
buffered: this.buffer.length,
|
|
679
746
|
dropped: this.dropped,
|
|
680
747
|
inFlight: this.inFlight,
|
|
@@ -684,6 +751,29 @@ var EventQueue = class {
|
|
|
684
751
|
nextRetryAt: this.nextRetryAt
|
|
685
752
|
};
|
|
686
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
|
+
}
|
|
687
777
|
// ---------- internal scheduling ----------
|
|
688
778
|
scheduleIdleFlush() {
|
|
689
779
|
this.cancelTimerIfSet();
|
|
@@ -717,6 +807,14 @@ function extractRetryAfterMs(err) {
|
|
|
717
807
|
}
|
|
718
808
|
return void 0;
|
|
719
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
|
+
}
|
|
720
818
|
function defaultScheduler(fn, ms) {
|
|
721
819
|
const id = setTimeout(fn, ms);
|
|
722
820
|
if (typeof id.unref === "function") {
|
|
@@ -1058,6 +1156,7 @@ var AutoTracker = class {
|
|
|
1058
1156
|
/** Exposed for tests + consumers that want to reset the session manually. */
|
|
1059
1157
|
resetSession() {
|
|
1060
1158
|
if (this.session && !this.session.endedSent) this.emitSessionEnd();
|
|
1159
|
+
this.pageviewId = null;
|
|
1061
1160
|
this.session = this.startNewSession();
|
|
1062
1161
|
this.emitSessionStart();
|
|
1063
1162
|
}
|
|
@@ -1094,6 +1193,7 @@ var AutoTracker = class {
|
|
|
1094
1193
|
const hiddenFor = this.session.hiddenAt ? Date.now() - this.session.hiddenAt : 0;
|
|
1095
1194
|
if (hiddenFor >= SESSION_RESUME_THRESHOLD_MS) {
|
|
1096
1195
|
this.emitSessionEnd();
|
|
1196
|
+
this.pageviewId = null;
|
|
1097
1197
|
this.session = this.startNewSession();
|
|
1098
1198
|
this.emitSessionStart();
|
|
1099
1199
|
} else {
|
|
@@ -1467,7 +1567,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1467
1567
|
const maxStringLength = options.maxStringLength ?? DEFAULT_MAX_STRING;
|
|
1468
1568
|
const maxBatchPropertyBytes = options.maxBatchPropertyBytes ?? DEFAULT_MAX_BYTES;
|
|
1469
1569
|
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
1470
|
-
const seen = /* @__PURE__ */ new
|
|
1570
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1471
1571
|
const visit = (value, key, depth) => {
|
|
1472
1572
|
if (depth > maxDepth) {
|
|
1473
1573
|
warnings.push({ kind: "depth_exceeded", key });
|
|
@@ -1555,6 +1655,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1555
1655
|
const result = visit(value[i], `${key}[${i}]`, depth + 1);
|
|
1556
1656
|
if (result.keep) out.push(result.value);
|
|
1557
1657
|
}
|
|
1658
|
+
seen.delete(value);
|
|
1558
1659
|
return { keep: true, value: out };
|
|
1559
1660
|
}
|
|
1560
1661
|
if (t === "object") {
|
|
@@ -1569,6 +1670,7 @@ function validateEventProperties(input, options = {}) {
|
|
|
1569
1670
|
const result = visit(obj[k], `${key}.${k}`, depth + 1);
|
|
1570
1671
|
if (result.keep) out[k] = result.value;
|
|
1571
1672
|
}
|
|
1673
|
+
seen.delete(obj);
|
|
1572
1674
|
return { keep: true, value: out };
|
|
1573
1675
|
}
|
|
1574
1676
|
warnings.push({ kind: "non_serialisable", key });
|
|
@@ -1911,35 +2013,27 @@ var ConsentManager = class {
|
|
|
1911
2013
|
};
|
|
1912
2014
|
var EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
1913
2015
|
var CARD_PATTERN = /\b\d(?:[ -]?\d){12,18}\b/g;
|
|
1914
|
-
var REPLACEMENT_EMAIL = "
|
|
1915
|
-
var REPLACEMENT_CARD = "
|
|
2016
|
+
var REPLACEMENT_EMAIL = "<email>";
|
|
2017
|
+
var REPLACEMENT_CARD = "<card>";
|
|
1916
2018
|
function scrubPii(value) {
|
|
1917
2019
|
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;
|
|
2020
|
+
return value.replace(EMAIL_PATTERN, REPLACEMENT_EMAIL).replace(CARD_PATTERN, REPLACEMENT_CARD);
|
|
1928
2021
|
}
|
|
1929
2022
|
function scrubPiiFromProperties(properties) {
|
|
1930
2023
|
const out = {};
|
|
1931
2024
|
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
|
-
}
|
|
2025
|
+
out[k] = scrubValue(properties[k]);
|
|
1940
2026
|
}
|
|
1941
2027
|
return out;
|
|
1942
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
|
+
}
|
|
1943
2037
|
|
|
1944
2038
|
// src/breadcrumbs.ts
|
|
1945
2039
|
var BreadcrumbBuffer = class {
|
|
@@ -2229,16 +2323,18 @@ var ErrorTracker = class {
|
|
|
2229
2323
|
const url = typeof input === "string" ? input : input?.url ?? "";
|
|
2230
2324
|
const method = (init.method || "GET").toUpperCase();
|
|
2231
2325
|
const start = Date.now();
|
|
2232
|
-
this.opts.
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
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
|
+
}
|
|
2238
2334
|
try {
|
|
2239
2335
|
const response = await origFetch(...args);
|
|
2240
2336
|
if (response.status >= 500 && this.opts.isConsented()) {
|
|
2241
|
-
if (!url.
|
|
2337
|
+
if (!isSelfRequest(url, this.opts.selfHostname)) {
|
|
2242
2338
|
this.captureHttp({
|
|
2243
2339
|
url,
|
|
2244
2340
|
method,
|
|
@@ -2287,7 +2383,7 @@ var ErrorTracker = class {
|
|
|
2287
2383
|
try {
|
|
2288
2384
|
if (xhr.status >= 500 && tracker.opts.isConsented()) {
|
|
2289
2385
|
const url = xhr._cdUrl ?? "";
|
|
2290
|
-
if (!url.
|
|
2386
|
+
if (!isSelfRequest(url, tracker.opts.selfHostname)) {
|
|
2291
2387
|
tracker.captureHttp({
|
|
2292
2388
|
url,
|
|
2293
2389
|
method: (xhr._cdMethod ?? "GET").toUpperCase(),
|
|
@@ -2447,9 +2543,10 @@ var ErrorTracker = class {
|
|
|
2447
2543
|
if (!this.passesSample(err)) return;
|
|
2448
2544
|
if (!this.passesRateLimit(err)) return;
|
|
2449
2545
|
let finalErr = err;
|
|
2450
|
-
|
|
2546
|
+
const hook = this.opts.beforeSend?.();
|
|
2547
|
+
if (hook) {
|
|
2451
2548
|
try {
|
|
2452
|
-
finalErr =
|
|
2549
|
+
finalErr = hook(err);
|
|
2453
2550
|
} catch {
|
|
2454
2551
|
finalErr = err;
|
|
2455
2552
|
}
|
|
@@ -2631,6 +2728,22 @@ function safeClone(v) {
|
|
|
2631
2728
|
function safeStringify2(v) {
|
|
2632
2729
|
return coerceErrorPayload(v).message;
|
|
2633
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
|
+
}
|
|
2634
2747
|
|
|
2635
2748
|
// src/crossdeck.ts
|
|
2636
2749
|
var CrossdeckClient = class {
|
|
@@ -2647,6 +2760,24 @@ var CrossdeckClient = class {
|
|
|
2647
2760
|
* mismatched env fails fast at boot rather than at first event-flush.
|
|
2648
2761
|
*/
|
|
2649
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
|
+
}
|
|
2650
2781
|
if (!options.publicKey || !options.publicKey.startsWith("cd_pub_")) {
|
|
2651
2782
|
throw new CrossdeckError({
|
|
2652
2783
|
type: "configuration_error",
|
|
@@ -2754,6 +2885,15 @@ var CrossdeckClient = class {
|
|
|
2754
2885
|
`Event flush failed (${info.lastError}). Retrying in ${info.delayMs}ms (attempt ${info.consecutiveFailures}).`,
|
|
2755
2886
|
{ ...info }
|
|
2756
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
|
+
);
|
|
2757
2897
|
}
|
|
2758
2898
|
});
|
|
2759
2899
|
const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
|
|
@@ -2820,8 +2960,20 @@ var CrossdeckClient = class {
|
|
|
2820
2960
|
report: (err) => this.reportError(err),
|
|
2821
2961
|
getContext: () => ({ ...this.state.errorContext }),
|
|
2822
2962
|
getTags: () => ({ ...this.state.errorTags }),
|
|
2823
|
-
|
|
2824
|
-
|
|
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)
|
|
2825
2977
|
});
|
|
2826
2978
|
this.state.errors = tracker;
|
|
2827
2979
|
tracker.install();
|
|
@@ -2904,7 +3056,8 @@ var CrossdeckClient = class {
|
|
|
2904
3056
|
body
|
|
2905
3057
|
});
|
|
2906
3058
|
const priorCdcust = s.identity.crossdeckCustomerId;
|
|
2907
|
-
|
|
3059
|
+
const cacheHasEntries = s.entitlements.list().length > 0;
|
|
3060
|
+
if (priorCdcust && result.crossdeckCustomerId && priorCdcust !== result.crossdeckCustomerId || !priorCdcust && cacheHasEntries) {
|
|
2908
3061
|
s.entitlements.clear();
|
|
2909
3062
|
}
|
|
2910
3063
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
@@ -3346,8 +3499,9 @@ var CrossdeckClient = class {
|
|
|
3346
3499
|
message: "syncPurchases requires a signedTransactionInfo string from StoreKit 2."
|
|
3347
3500
|
});
|
|
3348
3501
|
}
|
|
3502
|
+
const rail = input.rail ?? "apple";
|
|
3349
3503
|
const result = await s.http.request("POST", "/purchases/sync", {
|
|
3350
|
-
body: {
|
|
3504
|
+
body: { ...input, rail }
|
|
3351
3505
|
});
|
|
3352
3506
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
3353
3507
|
s.entitlements.setFromList(result.entitlements);
|
|
@@ -3413,6 +3567,8 @@ var CrossdeckClient = class {
|
|
|
3413
3567
|
this.state.errorContext = {};
|
|
3414
3568
|
this.state.errorTags = {};
|
|
3415
3569
|
this.state.developerUserId = null;
|
|
3570
|
+
this.state.lastServerTime = null;
|
|
3571
|
+
this.state.lastClientTime = null;
|
|
3416
3572
|
if (this.state.autoTracker) {
|
|
3417
3573
|
const tracker = new AutoTracker(
|
|
3418
3574
|
this.state.options.autoTrack,
|
|
@@ -3541,7 +3697,9 @@ function isLocalHostname() {
|
|
|
3541
3697
|
const hostname = w?.location?.hostname;
|
|
3542
3698
|
if (!hostname) return false;
|
|
3543
3699
|
if (hostname === "localhost" || hostname === "127.0.0.1") return true;
|
|
3700
|
+
if (hostname === "0.0.0.0") return true;
|
|
3544
3701
|
if (hostname === "::1" || hostname === "[::1]") return true;
|
|
3702
|
+
if (/^\[?fe80::/i.test(hostname)) return true;
|
|
3545
3703
|
if (hostname.endsWith(".local")) return true;
|
|
3546
3704
|
if (/^10\./.test(hostname)) return true;
|
|
3547
3705
|
if (/^192\.168\./.test(hostname)) return true;
|
|
@@ -3590,6 +3748,39 @@ function installUnloadFlush(onUnload) {
|
|
|
3590
3748
|
}
|
|
3591
3749
|
|
|
3592
3750
|
// src/react.ts
|
|
3751
|
+
var _moduleInitDone = false;
|
|
3752
|
+
function CrossdeckProvider(props) {
|
|
3753
|
+
const { userId, children, ...initOptions } = props;
|
|
3754
|
+
const lastUserIdRef = (0, import_react.useRef)(void 0);
|
|
3755
|
+
(0, import_react.useEffect)(() => {
|
|
3756
|
+
if (_moduleInitDone) return;
|
|
3757
|
+
try {
|
|
3758
|
+
Crossdeck.init(initOptions);
|
|
3759
|
+
_moduleInitDone = true;
|
|
3760
|
+
} catch (err) {
|
|
3761
|
+
if (typeof console !== "undefined") {
|
|
3762
|
+
console.error("[CrossdeckProvider] init failed:", err);
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
}, []);
|
|
3766
|
+
(0, import_react.useEffect)(() => {
|
|
3767
|
+
if (!_moduleInitDone) return;
|
|
3768
|
+
if (lastUserIdRef.current === userId) return;
|
|
3769
|
+
lastUserIdRef.current = userId;
|
|
3770
|
+
try {
|
|
3771
|
+
if (userId) {
|
|
3772
|
+
void Crossdeck.identify(userId);
|
|
3773
|
+
} else {
|
|
3774
|
+
Crossdeck.reset();
|
|
3775
|
+
}
|
|
3776
|
+
} catch (err) {
|
|
3777
|
+
if (typeof console !== "undefined") {
|
|
3778
|
+
console.error("[CrossdeckProvider] identity sync failed:", err);
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
}, [userId]);
|
|
3782
|
+
return children;
|
|
3783
|
+
}
|
|
3593
3784
|
function useEntitlement(key) {
|
|
3594
3785
|
const [isEntitled, setIsEntitled] = (0, import_react.useState)(() => safeIsEntitled(key));
|
|
3595
3786
|
(0, import_react.useEffect)(() => {
|
|
@@ -3640,6 +3831,7 @@ function safeListKeys() {
|
|
|
3640
3831
|
}
|
|
3641
3832
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3642
3833
|
0 && (module.exports = {
|
|
3834
|
+
CrossdeckProvider,
|
|
3643
3835
|
useEntitlement,
|
|
3644
3836
|
useEntitlements
|
|
3645
3837
|
});
|