@cross-deck/web 0.6.0 → 0.7.0
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/dist/index.cjs +271 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +23 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.mjs +271 -12
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +271 -12
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +271 -12
- package/dist/react.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -148,6 +148,15 @@ interface AutoTrackOptions {
|
|
|
148
148
|
pageViews: boolean;
|
|
149
149
|
/** Auto-attach os/browser/locale/screen/etc to every event's `properties`. Default true (browser only). */
|
|
150
150
|
deviceInfo: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Click autocapture — fire `element.clicked` for every interactive
|
|
153
|
+
* click on the page. Default true. Mixpanel/Amplitude pattern. Powers
|
|
154
|
+
* Crossdeck's funnel-attribution USP ("clicked X then converted").
|
|
155
|
+
* Privacy: skips form inputs / password fields / [class~="cd-noTrack"]
|
|
156
|
+
* subtrees. Override on individual elements with data-cd-event="custom"
|
|
157
|
+
* or data-cd-prop-* for custom property tagging.
|
|
158
|
+
*/
|
|
159
|
+
clicks: boolean;
|
|
151
160
|
}
|
|
152
161
|
/** Minimal interface for any pluggable key-value persistence. */
|
|
153
162
|
interface KeyValueStorage {
|
|
@@ -402,7 +411,20 @@ declare class CrossdeckClient {
|
|
|
402
411
|
* — matches the resolveCrossdeckCustomerId precedence on the server.
|
|
403
412
|
*/
|
|
404
413
|
private identityQueryParams;
|
|
405
|
-
/**
|
|
414
|
+
/**
|
|
415
|
+
* Embed every known identity axis on the event. Earlier this returned
|
|
416
|
+
* just the highest-priority hint (cdcust → developerUserId → anonymousId)
|
|
417
|
+
* to keep payloads small, but that leaked into analytics: once a user
|
|
418
|
+
* was logged in, every subsequent page.viewed shipped without
|
|
419
|
+
* anonymousId, and `uniqExact(anonymous_id)` on the warehouse side
|
|
420
|
+
* counted 0 visitors for the entire authenticated app.
|
|
421
|
+
*
|
|
422
|
+
* Bank-grade rule: the server is the single source of truth on
|
|
423
|
+
* dedup. Send everything we know; let CH count by whichever axis
|
|
424
|
+
* matches the question. Each field is at most 32 bytes — sending
|
|
425
|
+
* three on every event costs ~80 bytes per request, which is
|
|
426
|
+
* trivial compared to the analytics correctness it buys.
|
|
427
|
+
*/
|
|
406
428
|
private identityHintForEvent;
|
|
407
429
|
private mintEventId;
|
|
408
430
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -148,6 +148,15 @@ interface AutoTrackOptions {
|
|
|
148
148
|
pageViews: boolean;
|
|
149
149
|
/** Auto-attach os/browser/locale/screen/etc to every event's `properties`. Default true (browser only). */
|
|
150
150
|
deviceInfo: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Click autocapture — fire `element.clicked` for every interactive
|
|
153
|
+
* click on the page. Default true. Mixpanel/Amplitude pattern. Powers
|
|
154
|
+
* Crossdeck's funnel-attribution USP ("clicked X then converted").
|
|
155
|
+
* Privacy: skips form inputs / password fields / [class~="cd-noTrack"]
|
|
156
|
+
* subtrees. Override on individual elements with data-cd-event="custom"
|
|
157
|
+
* or data-cd-prop-* for custom property tagging.
|
|
158
|
+
*/
|
|
159
|
+
clicks: boolean;
|
|
151
160
|
}
|
|
152
161
|
/** Minimal interface for any pluggable key-value persistence. */
|
|
153
162
|
interface KeyValueStorage {
|
|
@@ -402,7 +411,20 @@ declare class CrossdeckClient {
|
|
|
402
411
|
* — matches the resolveCrossdeckCustomerId precedence on the server.
|
|
403
412
|
*/
|
|
404
413
|
private identityQueryParams;
|
|
405
|
-
/**
|
|
414
|
+
/**
|
|
415
|
+
* Embed every known identity axis on the event. Earlier this returned
|
|
416
|
+
* just the highest-priority hint (cdcust → developerUserId → anonymousId)
|
|
417
|
+
* to keep payloads small, but that leaked into analytics: once a user
|
|
418
|
+
* was logged in, every subsequent page.viewed shipped without
|
|
419
|
+
* anonymousId, and `uniqExact(anonymous_id)` on the warehouse side
|
|
420
|
+
* counted 0 visitors for the entire authenticated app.
|
|
421
|
+
*
|
|
422
|
+
* Bank-grade rule: the server is the single source of truth on
|
|
423
|
+
* dedup. Send everything we know; let CH count by whichever axis
|
|
424
|
+
* matches the question. Each field is at most 32 bytes — sending
|
|
425
|
+
* three on every event costs ~80 bytes per request, which is
|
|
426
|
+
* trivial compared to the analytics correctness it buys.
|
|
427
|
+
*/
|
|
406
428
|
private identityHintForEvent;
|
|
407
429
|
private mintEventId;
|
|
408
430
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -62,6 +62,9 @@ var HttpClient = class {
|
|
|
62
62
|
* - JSON parse failure on a 2xx (treated as `internal_error`)
|
|
63
63
|
*/
|
|
64
64
|
async request(method, path, options = {}) {
|
|
65
|
+
if (this.config.localDevMode) {
|
|
66
|
+
return synthesizeLocalDevResponse(path);
|
|
67
|
+
}
|
|
65
68
|
const url = this.buildUrl(path, options.query);
|
|
66
69
|
const headers = {
|
|
67
70
|
Authorization: `Bearer ${this.config.publicKey}`,
|
|
@@ -104,6 +107,14 @@ var HttpClient = class {
|
|
|
104
107
|
});
|
|
105
108
|
}
|
|
106
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Whether this client is in localhost dev-mode short-circuit. Used
|
|
112
|
+
* by other SDK pieces (event-queue) to skip network-bound work
|
|
113
|
+
* entirely rather than going through synthesizeLocalDevResponse.
|
|
114
|
+
*/
|
|
115
|
+
get isLocalDevMode() {
|
|
116
|
+
return this.config.localDevMode === true;
|
|
117
|
+
}
|
|
107
118
|
buildUrl(path, query) {
|
|
108
119
|
const base = this.config.baseUrl.replace(/\/+$/, "");
|
|
109
120
|
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
@@ -119,6 +130,49 @@ var HttpClient = class {
|
|
|
119
130
|
return url;
|
|
120
131
|
}
|
|
121
132
|
};
|
|
133
|
+
var cachedLocalCdcust = null;
|
|
134
|
+
function synthesizeLocalDevResponse(path) {
|
|
135
|
+
if (path.startsWith("/sdk/heartbeat")) {
|
|
136
|
+
return {
|
|
137
|
+
object: "heartbeat",
|
|
138
|
+
ok: true,
|
|
139
|
+
projectId: "proj_local_dev",
|
|
140
|
+
appId: "app_local_dev",
|
|
141
|
+
platform: "web",
|
|
142
|
+
env: "sandbox",
|
|
143
|
+
serverTime: Date.now()
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (path.startsWith("/identity/alias")) {
|
|
147
|
+
if (!cachedLocalCdcust) {
|
|
148
|
+
const tail = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID().replace(/-/g, "").slice(0, 16) : Math.random().toString(36).slice(2, 18);
|
|
149
|
+
cachedLocalCdcust = `cdcust_local_${tail}`;
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
object: "alias_result",
|
|
153
|
+
crossdeckCustomerId: cachedLocalCdcust,
|
|
154
|
+
linked: [],
|
|
155
|
+
mergePending: false,
|
|
156
|
+
env: "sandbox"
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (path.startsWith("/entitlements")) {
|
|
160
|
+
return {
|
|
161
|
+
object: "list",
|
|
162
|
+
data: [],
|
|
163
|
+
crossdeckCustomerId: cachedLocalCdcust ?? "",
|
|
164
|
+
env: "sandbox"
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (path.startsWith("/events")) {
|
|
168
|
+
return {
|
|
169
|
+
object: "list",
|
|
170
|
+
received: 0,
|
|
171
|
+
env: "sandbox"
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return {};
|
|
175
|
+
}
|
|
122
176
|
|
|
123
177
|
// src/identity.ts
|
|
124
178
|
var KEY_ANON = "anon_id";
|
|
@@ -605,7 +659,8 @@ function parseUserAgent(ua) {
|
|
|
605
659
|
var DEFAULT_AUTO_TRACK = {
|
|
606
660
|
sessions: true,
|
|
607
661
|
pageViews: true,
|
|
608
|
-
deviceInfo: true
|
|
662
|
+
deviceInfo: true,
|
|
663
|
+
clicks: true
|
|
609
664
|
};
|
|
610
665
|
var SESSION_RESUME_THRESHOLD_MS = 30 * 60 * 1e3;
|
|
611
666
|
var EMPTY_ACQUISITION = {
|
|
@@ -627,6 +682,7 @@ var AutoTracker = class {
|
|
|
627
682
|
if (!isBrowserSafe()) return;
|
|
628
683
|
if (this.cfg.sessions) this.installSessionTracking();
|
|
629
684
|
if (this.cfg.pageViews) this.installPageViewTracking();
|
|
685
|
+
if (this.cfg.clicks) this.installClickTracking();
|
|
630
686
|
}
|
|
631
687
|
uninstall() {
|
|
632
688
|
while (this.cleanups.length) {
|
|
@@ -721,11 +777,19 @@ var AutoTracker = class {
|
|
|
721
777
|
installPageViewTracking() {
|
|
722
778
|
const w = globalThis.window;
|
|
723
779
|
const doc = globalThis.document;
|
|
724
|
-
|
|
780
|
+
let lastFiredAt = 0;
|
|
781
|
+
let lastFiredUrl = "";
|
|
782
|
+
const DEDUP_WINDOW_MS = 250;
|
|
783
|
+
const fire = (force = false) => {
|
|
725
784
|
const loc = w.location;
|
|
785
|
+
const url = loc.href;
|
|
786
|
+
const now = Date.now();
|
|
787
|
+
if (!force && url === lastFiredUrl && now - lastFiredAt < DEDUP_WINDOW_MS) return;
|
|
788
|
+
lastFiredAt = now;
|
|
789
|
+
lastFiredUrl = url;
|
|
726
790
|
this.track("page.viewed", {
|
|
727
791
|
path: loc.pathname,
|
|
728
|
-
url
|
|
792
|
+
url,
|
|
729
793
|
search: loc.search || void 0,
|
|
730
794
|
hash: loc.hash || void 0,
|
|
731
795
|
title: doc.title,
|
|
@@ -747,7 +811,7 @@ var AutoTracker = class {
|
|
|
747
811
|
}
|
|
748
812
|
w.history.pushState = patchedPush;
|
|
749
813
|
w.history.replaceState = patchedReplace;
|
|
750
|
-
const onPopState = () => fire();
|
|
814
|
+
const onPopState = () => fire(true);
|
|
751
815
|
w.addEventListener("popstate", onPopState);
|
|
752
816
|
this.cleanups.push(() => {
|
|
753
817
|
if (w.history.pushState === patchedPush) {
|
|
@@ -759,7 +823,156 @@ var AutoTracker = class {
|
|
|
759
823
|
w.removeEventListener("popstate", onPopState);
|
|
760
824
|
});
|
|
761
825
|
}
|
|
826
|
+
// ---------- click autocapture ----------
|
|
827
|
+
/**
|
|
828
|
+
* Global click tracking — Mixpanel / Amplitude style autocapture.
|
|
829
|
+
* Fires `element.clicked` for every interactive click with the
|
|
830
|
+
* target element's selector path, text content, tag, href, data-*
|
|
831
|
+
* attributes, and viewport coordinates. Powers the funnel /
|
|
832
|
+
* attribution USP: "users who clicked X then converted within
|
|
833
|
+
* 7 days." Default ON because behavioural attribution is the
|
|
834
|
+
* core product promise.
|
|
835
|
+
*
|
|
836
|
+
* Privacy guardrails:
|
|
837
|
+
* - Skip clicks ON inputs / textareas / selects (form interaction
|
|
838
|
+
* isn't button telemetry; the dev should track form submits
|
|
839
|
+
* deliberately via track('form_submitted'))
|
|
840
|
+
* - Skip clicks INSIDE [type="password"] and password-class
|
|
841
|
+
* elements
|
|
842
|
+
* - Skip clicks inside elements opted out via class="cd-noTrack"
|
|
843
|
+
* or data-cd-noTrack attribute (Mixpanel's exact opt-out
|
|
844
|
+
* idiom — most devs already know it)
|
|
845
|
+
* - Capture text content but cap at 64 chars and trim — never
|
|
846
|
+
* more than what you'd see on a button label
|
|
847
|
+
*
|
|
848
|
+
* Volume guardrails:
|
|
849
|
+
* - Coalesce double-clicks within 100ms (React's synthetic click
|
|
850
|
+
* pattern + browser's native dblclick can fire twice)
|
|
851
|
+
* - Listen on document at capture phase so we see the click
|
|
852
|
+
* before any framework's own handlers stop propagation
|
|
853
|
+
*/
|
|
854
|
+
installClickTracking() {
|
|
855
|
+
const w = globalThis.window;
|
|
856
|
+
const doc = globalThis.document;
|
|
857
|
+
let lastFiredAt = 0;
|
|
858
|
+
let lastFiredTarget = null;
|
|
859
|
+
const COALESCE_MS = 100;
|
|
860
|
+
const TEXT_CAP = 64;
|
|
861
|
+
const onClick = (ev) => {
|
|
862
|
+
const target = ev.target;
|
|
863
|
+
if (!target || !(target instanceof Element)) return;
|
|
864
|
+
const now = Date.now();
|
|
865
|
+
if (target === lastFiredTarget && now - lastFiredAt < COALESCE_MS) return;
|
|
866
|
+
lastFiredAt = now;
|
|
867
|
+
lastFiredTarget = target;
|
|
868
|
+
const actionable = closestActionable(target);
|
|
869
|
+
const clicked = actionable || target;
|
|
870
|
+
if (isFormInput(clicked)) return;
|
|
871
|
+
if (isInOptedOut(clicked)) return;
|
|
872
|
+
if (isInsidePasswordField(clicked)) return;
|
|
873
|
+
const tag = clicked.tagName.toLowerCase();
|
|
874
|
+
const text = trimText(extractText(clicked), TEXT_CAP);
|
|
875
|
+
const href = clicked.href || void 0;
|
|
876
|
+
const linkTarget = clicked.target || void 0;
|
|
877
|
+
const elementId = clicked.id || void 0;
|
|
878
|
+
const role = clicked.getAttribute("role") || void 0;
|
|
879
|
+
const ariaLabel = clicked.getAttribute("aria-label") || void 0;
|
|
880
|
+
const selector = buildSelector(clicked);
|
|
881
|
+
const dataAttrs = collectDataAttrs(clicked);
|
|
882
|
+
const isLink = tag === "a" && !!href;
|
|
883
|
+
const explicitName = clicked.getAttribute("data-cd-event");
|
|
884
|
+
const props = {
|
|
885
|
+
selector,
|
|
886
|
+
tag,
|
|
887
|
+
text,
|
|
888
|
+
elementId,
|
|
889
|
+
role,
|
|
890
|
+
ariaLabel,
|
|
891
|
+
href,
|
|
892
|
+
isLink,
|
|
893
|
+
linkTarget,
|
|
894
|
+
viewportX: ev.clientX,
|
|
895
|
+
viewportY: ev.clientY,
|
|
896
|
+
pageX: ev.pageX,
|
|
897
|
+
pageY: ev.pageY,
|
|
898
|
+
...dataAttrs
|
|
899
|
+
};
|
|
900
|
+
for (const k of Object.keys(props)) {
|
|
901
|
+
if (props[k] === void 0 || props[k] === null || props[k] === "") delete props[k];
|
|
902
|
+
}
|
|
903
|
+
this.track(explicitName || "element.clicked", props);
|
|
904
|
+
};
|
|
905
|
+
doc.addEventListener("click", onClick, { capture: true, passive: true });
|
|
906
|
+
this.cleanups.push(() => {
|
|
907
|
+
doc.removeEventListener("click", onClick, { capture: true });
|
|
908
|
+
});
|
|
909
|
+
}
|
|
762
910
|
};
|
|
911
|
+
function closestActionable(el) {
|
|
912
|
+
return el.closest("[data-cd-event]") || el.closest("[data-cd-noTrack]") || el.closest("button, a, [role='button'], [role='link'], input[type='button'], input[type='submit']") || null;
|
|
913
|
+
}
|
|
914
|
+
function isFormInput(el) {
|
|
915
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
916
|
+
const tag = el.tagName.toLowerCase();
|
|
917
|
+
if (tag === "textarea" || tag === "select") return true;
|
|
918
|
+
if (tag === "input") {
|
|
919
|
+
const type = (el.type || "").toLowerCase();
|
|
920
|
+
return type !== "button" && type !== "submit" && type !== "image" && type !== "reset";
|
|
921
|
+
}
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
function isInOptedOut(el) {
|
|
925
|
+
if (el.closest("[data-cd-noTrack], [data-cd-no-track], .cd-noTrack, .cd-no-track")) return true;
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
function isInsidePasswordField(el) {
|
|
929
|
+
if (el.closest('input[type="password"]')) return true;
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
function extractText(el) {
|
|
933
|
+
const aria = el.getAttribute("aria-label");
|
|
934
|
+
if (aria) return aria.replace(/\s+/g, " ").trim();
|
|
935
|
+
if (el instanceof HTMLInputElement && el.value) return el.value;
|
|
936
|
+
const text = (el.textContent || "").replace(/\s+/g, " ").trim();
|
|
937
|
+
return text;
|
|
938
|
+
}
|
|
939
|
+
function trimText(s, cap) {
|
|
940
|
+
if (s.length <= cap) return s;
|
|
941
|
+
return s.slice(0, cap - 1) + "\u2026";
|
|
942
|
+
}
|
|
943
|
+
function buildSelector(el) {
|
|
944
|
+
const parts = [];
|
|
945
|
+
let cur = el;
|
|
946
|
+
let depth = 0;
|
|
947
|
+
while (cur && cur.nodeName.toLowerCase() !== "body" && depth < 5) {
|
|
948
|
+
let part = cur.nodeName.toLowerCase();
|
|
949
|
+
if (cur.id) {
|
|
950
|
+
parts.unshift(`${part}#${cur.id}`);
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
if (cur.classList.length > 0) {
|
|
954
|
+
const cls = Array.from(cur.classList).filter((c) => !c.startsWith("cd-")).slice(0, 2).join(".");
|
|
955
|
+
if (cls) part += `.${cls}`;
|
|
956
|
+
}
|
|
957
|
+
parts.unshift(part);
|
|
958
|
+
cur = cur.parentElement;
|
|
959
|
+
depth++;
|
|
960
|
+
}
|
|
961
|
+
return parts.join(" > ");
|
|
962
|
+
}
|
|
963
|
+
function collectDataAttrs(el) {
|
|
964
|
+
const out = {};
|
|
965
|
+
if (!(el instanceof HTMLElement)) return out;
|
|
966
|
+
for (const name of el.getAttributeNames()) {
|
|
967
|
+
if (!name.startsWith("data-")) continue;
|
|
968
|
+
if (name === "data-cd-noTrack" || name === "data-cd-no-track") continue;
|
|
969
|
+
if (name === "data-cd-event") continue;
|
|
970
|
+
const value = el.getAttribute(name) || "";
|
|
971
|
+
const key = name.replace(/^data-cd-prop-/, "").replace(/^data-/, "");
|
|
972
|
+
out[key] = value;
|
|
973
|
+
}
|
|
974
|
+
return out;
|
|
975
|
+
}
|
|
763
976
|
function isBrowserSafe() {
|
|
764
977
|
return typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
|
|
765
978
|
}
|
|
@@ -879,6 +1092,7 @@ var CrossdeckClient = class {
|
|
|
879
1092
|
message: `Crossdeck.init: environment "${options.environment}" disagrees with key prefix (${keyEnv}). Reconcile the publishable key with the environment declaration.`
|
|
880
1093
|
});
|
|
881
1094
|
}
|
|
1095
|
+
const localDevMode = isLocalHostname();
|
|
882
1096
|
const storage = options.storage ?? detectDefaultStorage();
|
|
883
1097
|
const persistIdentity = options.persistIdentity ?? true;
|
|
884
1098
|
const autoTrack = resolveAutoTrack(options.autoTrack);
|
|
@@ -905,8 +1119,18 @@ var CrossdeckClient = class {
|
|
|
905
1119
|
const http = new HttpClient({
|
|
906
1120
|
publicKey: opts.publicKey,
|
|
907
1121
|
baseUrl: opts.baseUrl,
|
|
908
|
-
sdkVersion: opts.sdkVersion
|
|
1122
|
+
sdkVersion: opts.sdkVersion,
|
|
1123
|
+
// Localhost auto-route: HttpClient short-circuits every request
|
|
1124
|
+
// to a successful no-op response when localDevMode is set.
|
|
1125
|
+
// SDK methods continue to work locally; nothing reaches the
|
|
1126
|
+
// server.
|
|
1127
|
+
localDevMode
|
|
909
1128
|
});
|
|
1129
|
+
if (localDevMode) {
|
|
1130
|
+
console.log(
|
|
1131
|
+
"[crossdeck] Localhost detected \u2014 running in dev mode (no network calls). Set publicKey: 'cd_pub_test_\u2026' and deploy to a real domain to test against the Crossdeck Sandbox."
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
910
1134
|
const effectiveStorage = persistIdentity ? storage : new MemoryStorage();
|
|
911
1135
|
const useCookieRedundancy = persistIdentity && !options.storage && // honour caller's adapter choice
|
|
912
1136
|
typeof globalThis.document !== "undefined";
|
|
@@ -959,7 +1183,7 @@ var CrossdeckClient = class {
|
|
|
959
1183
|
this.state.uninstallUnloadFlush = installUnloadFlush(() => {
|
|
960
1184
|
void this.flush({ keepalive: true }).catch(() => void 0);
|
|
961
1185
|
});
|
|
962
|
-
if (opts.autoHeartbeat) {
|
|
1186
|
+
if (opts.autoHeartbeat && !localDevMode) {
|
|
963
1187
|
void this.heartbeat().catch(() => void 0);
|
|
964
1188
|
}
|
|
965
1189
|
}
|
|
@@ -1193,6 +1417,12 @@ var CrossdeckClient = class {
|
|
|
1193
1417
|
*/
|
|
1194
1418
|
reset() {
|
|
1195
1419
|
if (!this.state) return;
|
|
1420
|
+
if (this.state.developerUserId) {
|
|
1421
|
+
try {
|
|
1422
|
+
this.track("user.signed_out", { auto: true });
|
|
1423
|
+
} catch {
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1196
1426
|
this.state.autoTracker?.uninstall();
|
|
1197
1427
|
this.state.identity.reset();
|
|
1198
1428
|
this.state.entitlements.clear();
|
|
@@ -1273,14 +1503,30 @@ var CrossdeckClient = class {
|
|
|
1273
1503
|
if (s.developerUserId) return { userId: s.developerUserId };
|
|
1274
1504
|
return { anonymousId: s.identity.anonymousId };
|
|
1275
1505
|
}
|
|
1276
|
-
/**
|
|
1506
|
+
/**
|
|
1507
|
+
* Embed every known identity axis on the event. Earlier this returned
|
|
1508
|
+
* just the highest-priority hint (cdcust → developerUserId → anonymousId)
|
|
1509
|
+
* to keep payloads small, but that leaked into analytics: once a user
|
|
1510
|
+
* was logged in, every subsequent page.viewed shipped without
|
|
1511
|
+
* anonymousId, and `uniqExact(anonymous_id)` on the warehouse side
|
|
1512
|
+
* counted 0 visitors for the entire authenticated app.
|
|
1513
|
+
*
|
|
1514
|
+
* Bank-grade rule: the server is the single source of truth on
|
|
1515
|
+
* dedup. Send everything we know; let CH count by whichever axis
|
|
1516
|
+
* matches the question. Each field is at most 32 bytes — sending
|
|
1517
|
+
* three on every event costs ~80 bytes per request, which is
|
|
1518
|
+
* trivial compared to the analytics correctness it buys.
|
|
1519
|
+
*/
|
|
1277
1520
|
identityHintForEvent() {
|
|
1278
1521
|
const s = this.requireStarted();
|
|
1522
|
+
const hint = {
|
|
1523
|
+
anonymousId: s.identity.anonymousId
|
|
1524
|
+
};
|
|
1525
|
+
if (s.developerUserId) hint.developerUserId = s.developerUserId;
|
|
1279
1526
|
if (s.identity.crossdeckCustomerId) {
|
|
1280
|
-
|
|
1527
|
+
hint.crossdeckCustomerId = s.identity.crossdeckCustomerId;
|
|
1281
1528
|
}
|
|
1282
|
-
|
|
1283
|
-
return { anonymousId: s.identity.anonymousId };
|
|
1529
|
+
return hint;
|
|
1284
1530
|
}
|
|
1285
1531
|
mintEventId() {
|
|
1286
1532
|
const ts = Date.now().toString(36);
|
|
@@ -1293,9 +1539,21 @@ function inferEnvFromKey(publicKey) {
|
|
|
1293
1539
|
if (publicKey.startsWith("cd_pub_live_")) return "production";
|
|
1294
1540
|
return null;
|
|
1295
1541
|
}
|
|
1542
|
+
function isLocalHostname() {
|
|
1543
|
+
const w = globalThis.window;
|
|
1544
|
+
const hostname = w?.location?.hostname;
|
|
1545
|
+
if (!hostname) return false;
|
|
1546
|
+
if (hostname === "localhost" || hostname === "127.0.0.1") return true;
|
|
1547
|
+
if (hostname === "::1" || hostname === "[::1]") return true;
|
|
1548
|
+
if (hostname.endsWith(".local")) return true;
|
|
1549
|
+
if (/^10\./.test(hostname)) return true;
|
|
1550
|
+
if (/^192\.168\./.test(hostname)) return true;
|
|
1551
|
+
if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(hostname)) return true;
|
|
1552
|
+
return false;
|
|
1553
|
+
}
|
|
1296
1554
|
function resolveAutoTrack(input) {
|
|
1297
1555
|
if (input === false) {
|
|
1298
|
-
return { sessions: false, pageViews: false, deviceInfo: false };
|
|
1556
|
+
return { sessions: false, pageViews: false, deviceInfo: false, clicks: false };
|
|
1299
1557
|
}
|
|
1300
1558
|
if (input === void 0 || input === true) {
|
|
1301
1559
|
return { ...DEFAULT_AUTO_TRACK };
|
|
@@ -1303,7 +1561,8 @@ function resolveAutoTrack(input) {
|
|
|
1303
1561
|
return {
|
|
1304
1562
|
sessions: input.sessions ?? DEFAULT_AUTO_TRACK.sessions,
|
|
1305
1563
|
pageViews: input.pageViews ?? DEFAULT_AUTO_TRACK.pageViews,
|
|
1306
|
-
deviceInfo: input.deviceInfo ?? DEFAULT_AUTO_TRACK.deviceInfo
|
|
1564
|
+
deviceInfo: input.deviceInfo ?? DEFAULT_AUTO_TRACK.deviceInfo,
|
|
1565
|
+
clicks: input.clicks ?? DEFAULT_AUTO_TRACK.clicks
|
|
1307
1566
|
};
|
|
1308
1567
|
}
|
|
1309
1568
|
function installUnloadFlush(onUnload) {
|