@cross-deck/web 0.5.0 → 0.6.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/CHANGELOG.md +35 -0
- package/README.md +19 -0
- package/dist/index.cjs +164 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +30 -2
- package/dist/index.d.ts +30 -2
- package/dist/index.mjs +164 -17
- package/dist/index.mjs.map +1 -1
- package/dist/react.cjs +164 -17
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +164 -17
- package/dist/react.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@cross-deck/web` will be documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
4
|
|
|
5
|
+
## [0.6.0] — 2026-05-10
|
|
6
|
+
|
|
7
|
+
Bank-grade analytics enrichment. Two additive changes that close the gap between Crossdeck's analytics surface and Google Analytics 4 / Google Ads dashboards: identity continuity that survives cleared storage, and first-touch acquisition attribution attached to every event of a session. No public API changes — `Crossdeck.init({...})` callsites do not need to change.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Identity continuity — dual-store redundancy.** The SDK now writes `anonymousId` and `crossdeckCustomerId` to BOTH `localStorage` (primary) and a 1st-party `document.cookie` (secondary). On boot it reads both and prefers primary; if primary is empty, it recovers from the cookie and resyncs primary. This protects against ITP localStorage purges, "clear site data" actions, and aggressive privacy extensions — a returning user keeps the same Crossdeck identity instead of becoming a phantom new visitor on dashboards. See `sdks/SDK_TRUTH.md` § "Identity continuity — bank-grade redundancy" for the full contract.
|
|
12
|
+
- **`CookieStorage` adapter** in `storage.ts`. Sets `Path=/`, `Max-Age=63072000` (2y), `SameSite=Lax`, `Secure` (when over HTTPS — omitted on `http://localhost` so dev works without a TLS cert). Encodes/decodes cookie names + values defensively so embedded `;` and `=` survive round-trip.
|
|
13
|
+
- **First-touch acquisition capture in `AutoTracker`.** On every `session.started` the SDK reads `window.location.search` and `document.referrer` and captures `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, plus `referrer`. Non-empty values are auto-attached to every subsequent event of that session — matching GA4's session-pinned attribution semantics. SPA route changes mid-session do NOT re-read the URL; a new session (>30 min idle, or explicit `resetSession()`) re-captures off the current URL.
|
|
14
|
+
- **`AutoTracker.currentAcquisition`** getter. Returns the captured-once-per-session acquisition context for inspection / tests / framework bindings. Returns empty strings (not undefined) when there's no active session so callers can spread without conditional logic.
|
|
15
|
+
- **`captureAcquisition()` exported** from `auto-track.ts` for unit testing acquisition extraction in isolation.
|
|
16
|
+
- **18 new tests** (138 total, up from 120):
|
|
17
|
+
- `storage.test.ts` — 6 cases covering `CookieStorage` round-trip, URL-encoding survival, attribute emission (Path / SameSite / Max-Age / Secure on HTTPS, Secure-omitted on HTTP), null on broken cookies, no-op in Node (no `document`).
|
|
18
|
+
- `identity.test.ts` — 6 cases covering the redundancy contract: writes-to-both, recovery from secondary when primary cleared, recovery from primary when secondary cleared, primary-wins-on-conflict, set/reset both, defence-in-depth against a throwing secondary.
|
|
19
|
+
- `auto-track.test.ts` — 5 cases: `captureAcquisition` reads utm_*, returns empty for clean URLs, `currentAcquisition` is session-pinned (SPA navigation does NOT change it mid-session), `resetSession` re-captures off the current URL, returns empty when no session exists.
|
|
20
|
+
|
|
21
|
+
### Server-side enrichment (lands without an SDK upgrade)
|
|
22
|
+
|
|
23
|
+
The 0.6.0 SDK pairs with these backend changes that started populating ClickHouse columns ahead of this release — every existing 0.5.0 install starts seeing them in dashboards immediately:
|
|
24
|
+
|
|
25
|
+
- **Geography** — `events.country` populated from the Cloudflare `CF-IPCountry` header at `/v1/events`. Server-decided, not client-trusted.
|
|
26
|
+
- **New vs returning** — `events.is_new` populated by a Firestore-transactional `visitors/{anonymousId}` upsert in the ClickHouse projector. First event for a new anonymousId wins the race; concurrent inserts converge.
|
|
27
|
+
- **Device hoist** — `events.browser`, `events.os`, `events.device_class` hoisted out of `properties_json` to first-class LowCardinality columns for fast slicing.
|
|
28
|
+
- **Acquisition columns** — `events.utm_source`, `events.utm_medium`, `events.utm_campaign`, `events.utm_content`, `events.utm_term`, `events.referrer_host` populated from event properties (which the 0.6.0 SDK now sends; pre-0.6.0 events get empty strings).
|
|
29
|
+
- **Sessions** — `sessions` table aggregates the same enrichment columns via `any` / `max` (for `is_new`) so per-session breakdowns don't have to fan out across raw events.
|
|
30
|
+
- ClickHouse migration `006_analytics_columns.sql` is idempotent and additive — old rows already in `events` keep working with empty / 0 defaults.
|
|
31
|
+
|
|
32
|
+
### Privacy posture
|
|
33
|
+
|
|
34
|
+
Privacy posture is unchanged from single-store identity. The cookie holds only the same `anonymousId` already in `localStorage` — no fingerprintable data, no PII. Anything that can read `localStorage` on the same origin can read this cookie; the security model is identical to Stripe, Segment, and PostHog's 1st-party identity cookies. `persistIdentity: false` continues to disable all persistence (in-memory only) for customers running strict consent flows.
|
|
35
|
+
|
|
36
|
+
### Compatibility
|
|
37
|
+
|
|
38
|
+
Source-compatible with 0.5.0. No public API changes. No deprecated symbols. Existing snippets do not need to change.
|
|
39
|
+
|
|
5
40
|
## [0.4.0] — 2026-05-09
|
|
6
41
|
|
|
7
42
|
Reactive entitlements. Pre-0.4.0, calling `Crossdeck.isEntitled("pro")` directly inside a React render path showed the empty-cache result forever — React had no way to know the cache had populated asynchronously after `init()`. This release closes that gap with a first-class subscribe API on the SDK and a React subpackage that uses it.
|
package/README.md
CHANGED
|
@@ -92,6 +92,8 @@ That's the full happy path.
|
|
|
92
92
|
|
|
93
93
|
Every event — auto-tracked and developer-emitted — is enriched with the device-info payload below. Quick tab switches (Cmd-Tab, switching browser tabs) don't end the session — only real closes do, matching GA4's session-window convention.
|
|
94
94
|
|
|
95
|
+
**Per-session acquisition (v0.6.0+):** when a session starts the SDK reads `window.location.search` and `document.referrer` and captures `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, plus `referrer`. Non-empty values are auto-attached to every subsequent event of that session — first-touch attribution stays pinned to the entry URL even after SPA route changes strip the params away. A new session (>30 min idle) re-reads the URL.
|
|
96
|
+
|
|
95
97
|
## Auto-attached device info
|
|
96
98
|
|
|
97
99
|
Every event's `properties` is enriched with whatever the SDK can detect:
|
|
@@ -296,6 +298,23 @@ Publishable keys aren't secrets — they're identifiers, safe to ship in client
|
|
|
296
298
|
- **Env partition** — a `cd_pub_live_…` key cannot read `cd_pub_test_…` data and vice versa.
|
|
297
299
|
- **No raw payment credentials** ever pass through this SDK or sit in a Crossdeck database. Apple `.p8`s, Stripe secret keys, Google service-account JSON — all in Google Cloud Secret Manager, runtime-only access from the Crossdeck backend.
|
|
298
300
|
|
|
301
|
+
## Identity & cookies (v0.6.0+)
|
|
302
|
+
|
|
303
|
+
The SDK persists `anonymousId` and `crossdeckCustomerId` so a returning user keeps the same Crossdeck identity across page loads. By default in browsers it writes to BOTH `localStorage` (primary) and a 1st-party `document.cookie` (secondary, `Path=/`, `Max-Age=2y`, `SameSite=Lax`, `Secure` over HTTPS). The redundancy keeps "10k unique visitors" actually meaning 10k humans even when one store is wiped by ITP, private browsing, or "clear site data."
|
|
304
|
+
|
|
305
|
+
The cookie holds only the same `anonymousId` already in `localStorage` — no fingerprintable data, no PII. Same security posture as Stripe, Segment, and PostHog's 1st-party identity cookies.
|
|
306
|
+
|
|
307
|
+
**Disabling persistence.** Customers running strict consent flows (e.g. cookies disabled until the visitor opts in via a consent banner) should pass `persistIdentity: false` to `Crossdeck.init`. That switches the SDK to in-memory only — no `localStorage`, no cookie, identity is recreated on every page load. Re-`init` with `persistIdentity: true` once consent lands.
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
Crossdeck.init({
|
|
311
|
+
appId, publicKey, environment,
|
|
312
|
+
persistIdentity: false, // strict consent — opt in later
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Cookie disclosure.** If your privacy policy enumerates cookies, list this one as a "1st-party functional / analytics cookie used to keep the same visitor identity across page loads." The Crossdeck cookie name uses the configured `storagePrefix` (default `crossdeck:`) followed by `anon_id` (and `cdcust_id` once a user signs in).
|
|
317
|
+
|
|
299
318
|
## Versioning
|
|
300
319
|
|
|
301
320
|
This package follows [semver](https://semver.org). The wire-format types (`PublicEntitlement`, `AliasResult`, etc.) are duplicated from the backend's `v1-types.ts` — they're the stable contract, not a shared module. Breaking changes to those types only ship in major versions.
|
package/dist/index.cjs
CHANGED
|
@@ -78,7 +78,7 @@ function typeMapForStatus(status) {
|
|
|
78
78
|
|
|
79
79
|
// src/http.ts
|
|
80
80
|
var SDK_NAME = "@cross-deck/web";
|
|
81
|
-
var SDK_VERSION = "0.
|
|
81
|
+
var SDK_VERSION = "0.6.0";
|
|
82
82
|
var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
83
83
|
var HttpClient = class {
|
|
84
84
|
constructor(config) {
|
|
@@ -156,19 +156,25 @@ var HttpClient = class {
|
|
|
156
156
|
var KEY_ANON = "anon_id";
|
|
157
157
|
var KEY_CDCUST = "cdcust_id";
|
|
158
158
|
var IdentityStore = class {
|
|
159
|
-
constructor(
|
|
160
|
-
this.
|
|
159
|
+
constructor(primary, prefix, secondary) {
|
|
160
|
+
this.primary = primary;
|
|
161
161
|
this.prefix = prefix;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
this.secondary = secondary ?? null;
|
|
163
|
+
const anonFromPrimary = primary.getItem(prefix + KEY_ANON);
|
|
164
|
+
const cdcustFromPrimary = primary.getItem(prefix + KEY_CDCUST);
|
|
165
|
+
const anonFromSecondary = this.secondary?.getItem(prefix + KEY_ANON) ?? null;
|
|
166
|
+
const cdcustFromSecondary = this.secondary?.getItem(prefix + KEY_CDCUST) ?? null;
|
|
167
|
+
const anon = anonFromPrimary ?? anonFromSecondary;
|
|
168
|
+
const cdcust = cdcustFromPrimary ?? cdcustFromSecondary;
|
|
166
169
|
this.state = {
|
|
167
|
-
anonymousId:
|
|
168
|
-
crossdeckCustomerId:
|
|
170
|
+
anonymousId: anon ?? this.mintAnonymousId(),
|
|
171
|
+
crossdeckCustomerId: cdcust
|
|
169
172
|
};
|
|
170
|
-
if (!
|
|
171
|
-
|
|
173
|
+
if (!anonFromPrimary || !anonFromSecondary) {
|
|
174
|
+
this.writeBoth(prefix + KEY_ANON, this.state.anonymousId);
|
|
175
|
+
}
|
|
176
|
+
if (cdcust && (!cdcustFromPrimary || !cdcustFromSecondary)) {
|
|
177
|
+
this.writeBoth(prefix + KEY_CDCUST, cdcust);
|
|
172
178
|
}
|
|
173
179
|
}
|
|
174
180
|
/** Return the persisted anonymous device ID (always set). */
|
|
@@ -182,7 +188,7 @@ var IdentityStore = class {
|
|
|
182
188
|
/** Persist a newly-resolved Crossdeck customer ID. */
|
|
183
189
|
setCrossdeckCustomerId(value) {
|
|
184
190
|
this.state.crossdeckCustomerId = value;
|
|
185
|
-
this.
|
|
191
|
+
this.writeBoth(this.prefix + KEY_CDCUST, value);
|
|
186
192
|
}
|
|
187
193
|
/**
|
|
188
194
|
* Wipe persisted identity. Called by reset() — used when an end-user
|
|
@@ -190,13 +196,13 @@ var IdentityStore = class {
|
|
|
190
196
|
* pre-login session is a fresh customer in the identity graph.
|
|
191
197
|
*/
|
|
192
198
|
reset() {
|
|
193
|
-
this.
|
|
194
|
-
this.
|
|
199
|
+
this.deleteBoth(this.prefix + KEY_ANON);
|
|
200
|
+
this.deleteBoth(this.prefix + KEY_CDCUST);
|
|
195
201
|
this.state = {
|
|
196
202
|
anonymousId: this.mintAnonymousId(),
|
|
197
203
|
crossdeckCustomerId: null
|
|
198
204
|
};
|
|
199
|
-
this.
|
|
205
|
+
this.writeBoth(this.prefix + KEY_ANON, this.state.anonymousId);
|
|
200
206
|
}
|
|
201
207
|
/**
|
|
202
208
|
* Generate an anonymousId. Crockford-ish base32 timestamp + random
|
|
@@ -208,6 +214,30 @@ var IdentityStore = class {
|
|
|
208
214
|
const rand = randomChars(10);
|
|
209
215
|
return `anon_${ts}${rand}`;
|
|
210
216
|
}
|
|
217
|
+
writeBoth(key, value) {
|
|
218
|
+
try {
|
|
219
|
+
this.primary.setItem(key, value);
|
|
220
|
+
} catch {
|
|
221
|
+
}
|
|
222
|
+
if (this.secondary) {
|
|
223
|
+
try {
|
|
224
|
+
this.secondary.setItem(key, value);
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
deleteBoth(key) {
|
|
230
|
+
try {
|
|
231
|
+
this.primary.removeItem(key);
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
if (this.secondary) {
|
|
235
|
+
try {
|
|
236
|
+
this.secondary.removeItem(key);
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
211
241
|
};
|
|
212
242
|
function randomChars(count) {
|
|
213
243
|
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
@@ -433,6 +463,59 @@ var MemoryStorage = class {
|
|
|
433
463
|
this.store.delete(key);
|
|
434
464
|
}
|
|
435
465
|
};
|
|
466
|
+
var CookieStorage = class {
|
|
467
|
+
constructor(options) {
|
|
468
|
+
this.maxAgeSec = options?.maxAgeSec ?? 63072e3;
|
|
469
|
+
this.secure = options?.secure ?? defaultSecure();
|
|
470
|
+
this.sameSite = options?.sameSite ?? "Lax";
|
|
471
|
+
}
|
|
472
|
+
getItem(key) {
|
|
473
|
+
if (!hasDocument()) return null;
|
|
474
|
+
const doc = globalThis.document;
|
|
475
|
+
const cookies = doc.cookie ? doc.cookie.split(/;\s*/) : [];
|
|
476
|
+
const prefix = encodeURIComponent(key) + "=";
|
|
477
|
+
for (const c of cookies) {
|
|
478
|
+
if (c.startsWith(prefix)) {
|
|
479
|
+
try {
|
|
480
|
+
return decodeURIComponent(c.slice(prefix.length));
|
|
481
|
+
} catch {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
setItem(key, value) {
|
|
489
|
+
if (!hasDocument()) return;
|
|
490
|
+
const doc = globalThis.document;
|
|
491
|
+
const parts = [
|
|
492
|
+
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
|
|
493
|
+
"Path=/",
|
|
494
|
+
`Max-Age=${this.maxAgeSec}`,
|
|
495
|
+
`SameSite=${this.sameSite}`
|
|
496
|
+
];
|
|
497
|
+
if (this.secure) parts.push("Secure");
|
|
498
|
+
try {
|
|
499
|
+
doc.cookie = parts.join("; ");
|
|
500
|
+
} catch {
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
removeItem(key) {
|
|
504
|
+
if (!hasDocument()) return;
|
|
505
|
+
const doc = globalThis.document;
|
|
506
|
+
const parts = [
|
|
507
|
+
`${encodeURIComponent(key)}=`,
|
|
508
|
+
"Path=/",
|
|
509
|
+
"Max-Age=0",
|
|
510
|
+
`SameSite=${this.sameSite}`
|
|
511
|
+
];
|
|
512
|
+
if (this.secure) parts.push("Secure");
|
|
513
|
+
try {
|
|
514
|
+
doc.cookie = parts.join("; ");
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
};
|
|
436
519
|
function detectDefaultStorage() {
|
|
437
520
|
try {
|
|
438
521
|
const ls = globalThis.localStorage;
|
|
@@ -446,6 +529,17 @@ function detectDefaultStorage() {
|
|
|
446
529
|
}
|
|
447
530
|
return new MemoryStorage();
|
|
448
531
|
}
|
|
532
|
+
function defaultSecure() {
|
|
533
|
+
try {
|
|
534
|
+
const loc = globalThis.location;
|
|
535
|
+
return loc?.protocol === "https:";
|
|
536
|
+
} catch {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function hasDocument() {
|
|
541
|
+
return typeof globalThis.document !== "undefined";
|
|
542
|
+
}
|
|
449
543
|
|
|
450
544
|
// src/device-info.ts
|
|
451
545
|
function isBrowser() {
|
|
@@ -546,6 +640,14 @@ var DEFAULT_AUTO_TRACK = {
|
|
|
546
640
|
deviceInfo: true
|
|
547
641
|
};
|
|
548
642
|
var SESSION_RESUME_THRESHOLD_MS = 30 * 60 * 1e3;
|
|
643
|
+
var EMPTY_ACQUISITION = {
|
|
644
|
+
utm_source: "",
|
|
645
|
+
utm_medium: "",
|
|
646
|
+
utm_campaign: "",
|
|
647
|
+
utm_content: "",
|
|
648
|
+
utm_term: "",
|
|
649
|
+
referrer: ""
|
|
650
|
+
};
|
|
549
651
|
var AutoTracker = class {
|
|
550
652
|
constructor(cfg, track) {
|
|
551
653
|
this.cfg = cfg;
|
|
@@ -581,6 +683,18 @@ var AutoTracker = class {
|
|
|
581
683
|
get currentSessionId() {
|
|
582
684
|
return this.session?.sessionId ?? null;
|
|
583
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* Per-session acquisition context — utm_* + referrer, captured once
|
|
688
|
+
* at session start. Returns empty strings when there's no session
|
|
689
|
+
* (Node, before init, after uninstall) so callers can spread without
|
|
690
|
+
* conditional logic. Bank-grade rule: capture once, attach to every
|
|
691
|
+
* event of the session, don't re-read on every track() (the URL
|
|
692
|
+
* changes via SPA pushState; the source-of-record is the URL we
|
|
693
|
+
* landed on).
|
|
694
|
+
*/
|
|
695
|
+
get currentAcquisition() {
|
|
696
|
+
return this.session?.acquisition ?? EMPTY_ACQUISITION;
|
|
697
|
+
}
|
|
584
698
|
// ---------- sessions ----------
|
|
585
699
|
installSessionTracking() {
|
|
586
700
|
this.session = this.startNewSession();
|
|
@@ -618,7 +732,8 @@ var AutoTracker = class {
|
|
|
618
732
|
sessionId: mintSessionId(),
|
|
619
733
|
startedAt: Date.now(),
|
|
620
734
|
hiddenAt: null,
|
|
621
|
-
endedSent: false
|
|
735
|
+
endedSent: false,
|
|
736
|
+
acquisition: captureAcquisition()
|
|
622
737
|
};
|
|
623
738
|
}
|
|
624
739
|
emitSessionStart() {
|
|
@@ -684,6 +799,26 @@ function mintSessionId() {
|
|
|
684
799
|
const ts = Date.now().toString(36);
|
|
685
800
|
return `sess_${ts}${randomChars(10)}`;
|
|
686
801
|
}
|
|
802
|
+
function captureAcquisition() {
|
|
803
|
+
if (!isBrowserSafe()) return { ...EMPTY_ACQUISITION };
|
|
804
|
+
const result = { ...EMPTY_ACQUISITION };
|
|
805
|
+
try {
|
|
806
|
+
const w = globalThis.window;
|
|
807
|
+
const params = new URLSearchParams(w.location.search ?? "");
|
|
808
|
+
result.utm_source = params.get("utm_source") ?? "";
|
|
809
|
+
result.utm_medium = params.get("utm_medium") ?? "";
|
|
810
|
+
result.utm_campaign = params.get("utm_campaign") ?? "";
|
|
811
|
+
result.utm_content = params.get("utm_content") ?? "";
|
|
812
|
+
result.utm_term = params.get("utm_term") ?? "";
|
|
813
|
+
} catch {
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
const doc = globalThis.document;
|
|
817
|
+
if (typeof doc.referrer === "string") result.referrer = doc.referrer;
|
|
818
|
+
} catch {
|
|
819
|
+
}
|
|
820
|
+
return result;
|
|
821
|
+
}
|
|
687
822
|
|
|
688
823
|
// src/debug.ts
|
|
689
824
|
var SENSITIVE_KEY_PATTERNS = [
|
|
@@ -805,7 +940,10 @@ var CrossdeckClient = class {
|
|
|
805
940
|
sdkVersion: opts.sdkVersion
|
|
806
941
|
});
|
|
807
942
|
const effectiveStorage = persistIdentity ? storage : new MemoryStorage();
|
|
808
|
-
const
|
|
943
|
+
const useCookieRedundancy = persistIdentity && !options.storage && // honour caller's adapter choice
|
|
944
|
+
typeof globalThis.document !== "undefined";
|
|
945
|
+
const cookieStore = useCookieRedundancy ? new CookieStorage() : void 0;
|
|
946
|
+
const identity = new IdentityStore(effectiveStorage, opts.storagePrefix, cookieStore);
|
|
809
947
|
const entitlements = new EntitlementCache();
|
|
810
948
|
const events = new EventQueue({
|
|
811
949
|
http,
|
|
@@ -986,6 +1124,15 @@ var CrossdeckClient = class {
|
|
|
986
1124
|
const enriched = { ...s.deviceInfo };
|
|
987
1125
|
const sessionId = s.autoTracker?.currentSessionId;
|
|
988
1126
|
if (sessionId) enriched.sessionId = sessionId;
|
|
1127
|
+
const acquisition = s.autoTracker?.currentAcquisition;
|
|
1128
|
+
if (acquisition) {
|
|
1129
|
+
if (acquisition.utm_source) enriched.utm_source = acquisition.utm_source;
|
|
1130
|
+
if (acquisition.utm_medium) enriched.utm_medium = acquisition.utm_medium;
|
|
1131
|
+
if (acquisition.utm_campaign) enriched.utm_campaign = acquisition.utm_campaign;
|
|
1132
|
+
if (acquisition.utm_content) enriched.utm_content = acquisition.utm_content;
|
|
1133
|
+
if (acquisition.utm_term) enriched.utm_term = acquisition.utm_term;
|
|
1134
|
+
if (acquisition.referrer) enriched.referrer = acquisition.referrer;
|
|
1135
|
+
}
|
|
989
1136
|
if (properties) Object.assign(enriched, properties);
|
|
990
1137
|
const event = {
|
|
991
1138
|
eventId: this.mintEventId(),
|