@cross-deck/web 0.1.0 → 0.2.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/README.md +37 -0
- package/dist/index.d.mts +64 -2
- package/dist/index.d.ts +64 -2
- package/dist/index.js +281 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +281 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -33,12 +33,46 @@ That's the full happy path.
|
|
|
33
33
|
|
|
34
34
|
## What it does
|
|
35
35
|
|
|
36
|
+
- **Auto-tracking, on by default.** Sessions, page views, and device info (OS, browser, locale, timezone, screen size, app version) ship from boot. No instrumentation needed for the basics. Disable any of them via `autoTrack: { sessions: false }` etc.
|
|
36
37
|
- **One identity for every device + user.** Pre-login events get an `anonymousId`. After login, `identify()` links them to your user ID through Crossdeck's identity graph. The SDK persists both so subsequent app launches resume where you left off.
|
|
37
38
|
- **Synchronous entitlement reads.** `getEntitlements()` populates a local cache. `isEntitled("pro")` is a Set lookup — no network call, no waiting.
|
|
38
39
|
- **Batched telemetry.** `track()` queues events in memory; the SDK flushes every 5 seconds (configurable) or when the buffer hits 20 events. Network failures re-queue the batch — events aren't lost on a flaky connection.
|
|
39
40
|
- **Boot heartbeat.** On `start()` the SDK pings `/v1/sdk/heartbeat` so the dashboard's Apps page can show you "last seen" per install. Disable with `autoHeartbeat: false`.
|
|
40
41
|
- **Stripe-style errors.** Every async method throws `CrossdeckError` with `type`, `code`, `requestId`, and `status` — same shape as Stripe's SDKs, so generic error handlers transfer.
|
|
41
42
|
|
|
43
|
+
## Auto-tracked events
|
|
44
|
+
|
|
45
|
+
| Event | When |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `session.started` | On boot. Carries `sessionId`. |
|
|
48
|
+
| `session.ended` | On `pagehide` / `beforeunload`, OR when returning to a tab after >30 min idle. Carries `sessionId` and `durationMs`. |
|
|
49
|
+
| `page.viewed` | On initial load + every SPA navigation (`history.pushState`, `replaceState`, `popstate`). Carries `path`, `url`, `search`, `hash`, `title`, `referrer`. |
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
## Auto-attached device info
|
|
54
|
+
|
|
55
|
+
Every event's `properties` is enriched with whatever the SDK can detect:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
{
|
|
59
|
+
os: "macOS" | "iOS" | "Android" | "Windows" | "Linux",
|
|
60
|
+
osVersion: "14.4",
|
|
61
|
+
browser: "Safari" | "Chrome" | "Firefox" | "Edge" | "Opera",
|
|
62
|
+
browserVersion: "17.5",
|
|
63
|
+
locale: "en-US",
|
|
64
|
+
timezone: "Africa/Johannesburg",
|
|
65
|
+
screenWidth: 2560,
|
|
66
|
+
screenHeight: 1440,
|
|
67
|
+
viewportWidth: 1440,
|
|
68
|
+
viewportHeight: 900,
|
|
69
|
+
devicePixelRatio: 2,
|
|
70
|
+
appVersion: "1.2.3", // only when you set Crossdeck.start({ appVersion })
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
No fingerprinting, no IP collection on the event document, no canvas hashing. Privacy by default. Caller-supplied properties always override auto-detected ones, so you can override `appVersion` per event if you A/B builds.
|
|
75
|
+
|
|
42
76
|
## API
|
|
43
77
|
|
|
44
78
|
### `Crossdeck.start(options)`
|
|
@@ -49,6 +83,9 @@ Boot the client. Idempotent — calling twice with the same options is fine.
|
|
|
49
83
|
Crossdeck.start({
|
|
50
84
|
publicKey: "cd_pub_live_…", // required
|
|
51
85
|
baseUrl: "https://api.cross-deck.com/v1", // override for self-host or emulator
|
|
86
|
+
appVersion: "1.2.3", // attached to every event as properties.appVersion
|
|
87
|
+
autoTrack: true, // default — sessions, page views, device info
|
|
88
|
+
// …or granular: autoTrack: { sessions: false } keeps page views + device info
|
|
52
89
|
autoHeartbeat: true, // default; set false for high-frequency boots
|
|
53
90
|
eventFlushBatchSize: 20, // default
|
|
54
91
|
eventFlushIntervalMs: 5_000, // default
|
package/dist/index.d.mts
CHANGED
|
@@ -90,6 +90,34 @@ interface CrossdeckOptions {
|
|
|
90
90
|
eventFlushIntervalMs?: number;
|
|
91
91
|
/** Override the SDK version reported on heartbeats. Default: package version. */
|
|
92
92
|
sdkVersion?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Auto-tracking. Default: every flag is `true` in browsers, all
|
|
95
|
+
* silently no-op in Node.
|
|
96
|
+
*
|
|
97
|
+
* Pass `false` to disable everything, or a partial object to override
|
|
98
|
+
* individual flags:
|
|
99
|
+
*
|
|
100
|
+
* Crossdeck.start({
|
|
101
|
+
* publicKey: "...",
|
|
102
|
+
* autoTrack: { pageViews: false }, // sessions + deviceInfo still on
|
|
103
|
+
* });
|
|
104
|
+
*/
|
|
105
|
+
autoTrack?: boolean | Partial<AutoTrackOptions>;
|
|
106
|
+
/**
|
|
107
|
+
* Your app's version (e.g. "1.2.3"). Auto-attached to every event as
|
|
108
|
+
* `properties.appVersion` when `autoTrack.deviceInfo` is enabled.
|
|
109
|
+
* Useful for slicing dashboards by build.
|
|
110
|
+
*/
|
|
111
|
+
appVersion?: string;
|
|
112
|
+
}
|
|
113
|
+
/** Auto-tracking flags. See CrossdeckOptions.autoTrack. */
|
|
114
|
+
interface AutoTrackOptions {
|
|
115
|
+
/** Emit `session.started` / `session.ended` automatically. Default true (browser only). */
|
|
116
|
+
sessions: boolean;
|
|
117
|
+
/** Emit `page.viewed` on initial load + SPA navigation. Default true (browser only). */
|
|
118
|
+
pageViews: boolean;
|
|
119
|
+
/** Auto-attach os/browser/locale/screen/etc to every event's `properties`. Default true (browser only). */
|
|
120
|
+
deviceInfo: boolean;
|
|
93
121
|
}
|
|
94
122
|
/** Minimal interface for any pluggable key-value persistence. */
|
|
95
123
|
interface KeyValueStorage {
|
|
@@ -300,7 +328,41 @@ declare class MemoryStorage implements KeyValueStorage {
|
|
|
300
328
|
* fetch shim, no transitive deps.
|
|
301
329
|
*/
|
|
302
330
|
declare const SDK_NAME = "@cross-deck/web";
|
|
303
|
-
declare const SDK_VERSION = "0.
|
|
331
|
+
declare const SDK_VERSION = "0.2.0";
|
|
304
332
|
declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
305
333
|
|
|
306
|
-
|
|
334
|
+
/**
|
|
335
|
+
* Device + environment enrichment.
|
|
336
|
+
*
|
|
337
|
+
* Auto-attached to every event the SDK emits when `autoTrack.deviceInfo` is
|
|
338
|
+
* enabled (default). Caller-supplied event properties always override
|
|
339
|
+
* auto-detected ones (so a developer can manually set `app.version` per
|
|
340
|
+
* event if they want to A/B between builds).
|
|
341
|
+
*
|
|
342
|
+
* Privacy posture:
|
|
343
|
+
* - No fingerprinting (no canvas hashes, no font enumeration).
|
|
344
|
+
* - No precise geolocation (only timezone + locale, both of which the
|
|
345
|
+
* browser exposes to every page anyway).
|
|
346
|
+
* - No IP collection — the backend logs the request IP for rate-limit
|
|
347
|
+
* purposes; it isn't stored on the event document.
|
|
348
|
+
* - All fields are typed enums or short strings; we never echo back
|
|
349
|
+
* full User-Agent strings to avoid surfacing fingerprintable detail
|
|
350
|
+
* in dashboards.
|
|
351
|
+
*/
|
|
352
|
+
interface DeviceInfo {
|
|
353
|
+
os?: string;
|
|
354
|
+
osVersion?: string;
|
|
355
|
+
browser?: string;
|
|
356
|
+
browserVersion?: string;
|
|
357
|
+
locale?: string;
|
|
358
|
+
timezone?: string;
|
|
359
|
+
screenWidth?: number;
|
|
360
|
+
screenHeight?: number;
|
|
361
|
+
viewportWidth?: number;
|
|
362
|
+
viewportHeight?: number;
|
|
363
|
+
devicePixelRatio?: number;
|
|
364
|
+
/** Caller-supplied. Set via Crossdeck.start({ appVersion: "1.2.3" }). */
|
|
365
|
+
appVersion?: string;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export { type AliasResult, type AuditRail, type AutoTrackOptions, Crossdeck, CrossdeckClient, CrossdeckError, type CrossdeckErrorPayload, type CrossdeckErrorType, type CrossdeckOptions, DEFAULT_BASE_URL, type DeviceInfo, type Diagnostics, type EntitlementsListResponse, type Environment, type EventProperties, type HeartbeatResponse, type IdentifyOptions, type KeyValueStorage, MemoryStorage, type Platform, type PublicEntitlement, type PurchaseResult, SDK_NAME, SDK_VERSION };
|
package/dist/index.d.ts
CHANGED
|
@@ -90,6 +90,34 @@ interface CrossdeckOptions {
|
|
|
90
90
|
eventFlushIntervalMs?: number;
|
|
91
91
|
/** Override the SDK version reported on heartbeats. Default: package version. */
|
|
92
92
|
sdkVersion?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Auto-tracking. Default: every flag is `true` in browsers, all
|
|
95
|
+
* silently no-op in Node.
|
|
96
|
+
*
|
|
97
|
+
* Pass `false` to disable everything, or a partial object to override
|
|
98
|
+
* individual flags:
|
|
99
|
+
*
|
|
100
|
+
* Crossdeck.start({
|
|
101
|
+
* publicKey: "...",
|
|
102
|
+
* autoTrack: { pageViews: false }, // sessions + deviceInfo still on
|
|
103
|
+
* });
|
|
104
|
+
*/
|
|
105
|
+
autoTrack?: boolean | Partial<AutoTrackOptions>;
|
|
106
|
+
/**
|
|
107
|
+
* Your app's version (e.g. "1.2.3"). Auto-attached to every event as
|
|
108
|
+
* `properties.appVersion` when `autoTrack.deviceInfo` is enabled.
|
|
109
|
+
* Useful for slicing dashboards by build.
|
|
110
|
+
*/
|
|
111
|
+
appVersion?: string;
|
|
112
|
+
}
|
|
113
|
+
/** Auto-tracking flags. See CrossdeckOptions.autoTrack. */
|
|
114
|
+
interface AutoTrackOptions {
|
|
115
|
+
/** Emit `session.started` / `session.ended` automatically. Default true (browser only). */
|
|
116
|
+
sessions: boolean;
|
|
117
|
+
/** Emit `page.viewed` on initial load + SPA navigation. Default true (browser only). */
|
|
118
|
+
pageViews: boolean;
|
|
119
|
+
/** Auto-attach os/browser/locale/screen/etc to every event's `properties`. Default true (browser only). */
|
|
120
|
+
deviceInfo: boolean;
|
|
93
121
|
}
|
|
94
122
|
/** Minimal interface for any pluggable key-value persistence. */
|
|
95
123
|
interface KeyValueStorage {
|
|
@@ -300,7 +328,41 @@ declare class MemoryStorage implements KeyValueStorage {
|
|
|
300
328
|
* fetch shim, no transitive deps.
|
|
301
329
|
*/
|
|
302
330
|
declare const SDK_NAME = "@cross-deck/web";
|
|
303
|
-
declare const SDK_VERSION = "0.
|
|
331
|
+
declare const SDK_VERSION = "0.2.0";
|
|
304
332
|
declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
305
333
|
|
|
306
|
-
|
|
334
|
+
/**
|
|
335
|
+
* Device + environment enrichment.
|
|
336
|
+
*
|
|
337
|
+
* Auto-attached to every event the SDK emits when `autoTrack.deviceInfo` is
|
|
338
|
+
* enabled (default). Caller-supplied event properties always override
|
|
339
|
+
* auto-detected ones (so a developer can manually set `app.version` per
|
|
340
|
+
* event if they want to A/B between builds).
|
|
341
|
+
*
|
|
342
|
+
* Privacy posture:
|
|
343
|
+
* - No fingerprinting (no canvas hashes, no font enumeration).
|
|
344
|
+
* - No precise geolocation (only timezone + locale, both of which the
|
|
345
|
+
* browser exposes to every page anyway).
|
|
346
|
+
* - No IP collection — the backend logs the request IP for rate-limit
|
|
347
|
+
* purposes; it isn't stored on the event document.
|
|
348
|
+
* - All fields are typed enums or short strings; we never echo back
|
|
349
|
+
* full User-Agent strings to avoid surfacing fingerprintable detail
|
|
350
|
+
* in dashboards.
|
|
351
|
+
*/
|
|
352
|
+
interface DeviceInfo {
|
|
353
|
+
os?: string;
|
|
354
|
+
osVersion?: string;
|
|
355
|
+
browser?: string;
|
|
356
|
+
browserVersion?: string;
|
|
357
|
+
locale?: string;
|
|
358
|
+
timezone?: string;
|
|
359
|
+
screenWidth?: number;
|
|
360
|
+
screenHeight?: number;
|
|
361
|
+
viewportWidth?: number;
|
|
362
|
+
viewportHeight?: number;
|
|
363
|
+
devicePixelRatio?: number;
|
|
364
|
+
/** Caller-supplied. Set via Crossdeck.start({ appVersion: "1.2.3" }). */
|
|
365
|
+
appVersion?: string;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export { type AliasResult, type AuditRail, type AutoTrackOptions, Crossdeck, CrossdeckClient, CrossdeckError, type CrossdeckErrorPayload, type CrossdeckErrorType, type CrossdeckOptions, DEFAULT_BASE_URL, type DeviceInfo, type Diagnostics, type EntitlementsListResponse, type Environment, type EventProperties, type HeartbeatResponse, type IdentifyOptions, type KeyValueStorage, MemoryStorage, type Platform, type PublicEntitlement, type PurchaseResult, SDK_NAME, SDK_VERSION };
|
package/dist/index.js
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.2.0";
|
|
82
82
|
var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
83
83
|
var HttpClient = class {
|
|
84
84
|
constructor(config) {
|
|
@@ -389,6 +389,244 @@ function detectDefaultStorage() {
|
|
|
389
389
|
return new MemoryStorage();
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
// src/device-info.ts
|
|
393
|
+
function isBrowser() {
|
|
394
|
+
return typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined" && typeof globalThis.navigator !== "undefined";
|
|
395
|
+
}
|
|
396
|
+
function collectDeviceInfo(extra) {
|
|
397
|
+
const info = {};
|
|
398
|
+
if (extra?.appVersion) info.appVersion = extra.appVersion;
|
|
399
|
+
if (!isBrowser()) return info;
|
|
400
|
+
const w = globalThis.window;
|
|
401
|
+
const nav = globalThis.navigator;
|
|
402
|
+
const doc = globalThis.document;
|
|
403
|
+
try {
|
|
404
|
+
if (typeof nav.language === "string") info.locale = nav.language;
|
|
405
|
+
} catch {
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
info.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
if (w.screen) {
|
|
413
|
+
info.screenWidth = w.screen.width;
|
|
414
|
+
info.screenHeight = w.screen.height;
|
|
415
|
+
}
|
|
416
|
+
info.viewportWidth = w.innerWidth;
|
|
417
|
+
info.viewportHeight = w.innerHeight;
|
|
418
|
+
info.devicePixelRatio = w.devicePixelRatio;
|
|
419
|
+
} catch {
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
const ua = nav.userAgent ?? "";
|
|
423
|
+
const parsed = parseUserAgent(ua);
|
|
424
|
+
Object.assign(info, parsed);
|
|
425
|
+
} catch {
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
const uaData = nav.userAgentData;
|
|
429
|
+
if (uaData?.platform && !info.os) info.os = uaData.platform;
|
|
430
|
+
if (uaData?.brands && !info.browser) {
|
|
431
|
+
const real = uaData.brands.find(
|
|
432
|
+
(b) => !/Not[ .;A]*Brand/i.test(b.brand) && !/Chromium/i.test(b.brand)
|
|
433
|
+
);
|
|
434
|
+
if (real) {
|
|
435
|
+
info.browser = real.brand;
|
|
436
|
+
info.browserVersion = real.version;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
} catch {
|
|
440
|
+
}
|
|
441
|
+
void doc;
|
|
442
|
+
return info;
|
|
443
|
+
}
|
|
444
|
+
function parseUserAgent(ua) {
|
|
445
|
+
const out = {};
|
|
446
|
+
if (/iPad|iPhone|iPod/.test(ua)) {
|
|
447
|
+
out.os = "iOS";
|
|
448
|
+
const m = ua.match(/OS (\d+[._]\d+(?:[._]\d+)?)/);
|
|
449
|
+
if (m?.[1]) out.osVersion = m[1].replace(/_/g, ".");
|
|
450
|
+
} else if (/Android/.test(ua)) {
|
|
451
|
+
out.os = "Android";
|
|
452
|
+
const m = ua.match(/Android (\d+(?:\.\d+)*)/);
|
|
453
|
+
if (m?.[1]) out.osVersion = m[1];
|
|
454
|
+
} else if (/Windows/.test(ua)) {
|
|
455
|
+
out.os = "Windows";
|
|
456
|
+
const m = ua.match(/Windows NT (\d+\.\d+)/);
|
|
457
|
+
if (m?.[1]) out.osVersion = m[1];
|
|
458
|
+
} else if (/Mac OS X|Macintosh/.test(ua)) {
|
|
459
|
+
out.os = "macOS";
|
|
460
|
+
const m = ua.match(/Mac OS X (\d+[._]\d+(?:[._]\d+)?)/);
|
|
461
|
+
if (m?.[1]) out.osVersion = m[1].replace(/_/g, ".");
|
|
462
|
+
} else if (/Linux/.test(ua)) {
|
|
463
|
+
out.os = "Linux";
|
|
464
|
+
}
|
|
465
|
+
if (/Edg\/(\d+(?:\.\d+)*)/.test(ua)) {
|
|
466
|
+
out.browser = "Edge";
|
|
467
|
+
out.browserVersion = ua.match(/Edg\/(\d+(?:\.\d+)*)/)?.[1];
|
|
468
|
+
} else if (/Firefox\/(\d+(?:\.\d+)*)/.test(ua)) {
|
|
469
|
+
out.browser = "Firefox";
|
|
470
|
+
out.browserVersion = ua.match(/Firefox\/(\d+(?:\.\d+)*)/)?.[1];
|
|
471
|
+
} else if (/OPR\/(\d+(?:\.\d+)*)/.test(ua)) {
|
|
472
|
+
out.browser = "Opera";
|
|
473
|
+
out.browserVersion = ua.match(/OPR\/(\d+(?:\.\d+)*)/)?.[1];
|
|
474
|
+
} else if (/Chrome\/(\d+(?:\.\d+)*)/.test(ua)) {
|
|
475
|
+
out.browser = "Chrome";
|
|
476
|
+
out.browserVersion = ua.match(/Chrome\/(\d+(?:\.\d+)*)/)?.[1];
|
|
477
|
+
} else if (/Version\/(\d+(?:\.\d+)*).*Safari/.test(ua)) {
|
|
478
|
+
out.browser = "Safari";
|
|
479
|
+
out.browserVersion = ua.match(/Version\/(\d+(?:\.\d+)*)/)?.[1];
|
|
480
|
+
}
|
|
481
|
+
return out;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/auto-track.ts
|
|
485
|
+
var DEFAULT_AUTO_TRACK = {
|
|
486
|
+
sessions: true,
|
|
487
|
+
pageViews: true,
|
|
488
|
+
deviceInfo: true
|
|
489
|
+
};
|
|
490
|
+
var SESSION_RESUME_THRESHOLD_MS = 30 * 60 * 1e3;
|
|
491
|
+
var AutoTracker = class {
|
|
492
|
+
constructor(cfg, track) {
|
|
493
|
+
this.cfg = cfg;
|
|
494
|
+
this.track = track;
|
|
495
|
+
this.session = null;
|
|
496
|
+
this.cleanups = [];
|
|
497
|
+
}
|
|
498
|
+
install() {
|
|
499
|
+
if (!isBrowserSafe()) return;
|
|
500
|
+
if (this.cfg.sessions) this.installSessionTracking();
|
|
501
|
+
if (this.cfg.pageViews) this.installPageViewTracking();
|
|
502
|
+
}
|
|
503
|
+
uninstall() {
|
|
504
|
+
while (this.cleanups.length) {
|
|
505
|
+
const fn = this.cleanups.pop();
|
|
506
|
+
try {
|
|
507
|
+
fn?.();
|
|
508
|
+
} catch {
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (this.session && !this.session.endedSent) {
|
|
512
|
+
this.emitSessionEnd();
|
|
513
|
+
}
|
|
514
|
+
this.session = null;
|
|
515
|
+
}
|
|
516
|
+
/** Exposed for tests + consumers that want to reset the session manually. */
|
|
517
|
+
resetSession() {
|
|
518
|
+
if (this.session && !this.session.endedSent) this.emitSessionEnd();
|
|
519
|
+
this.session = this.startNewSession();
|
|
520
|
+
this.emitSessionStart();
|
|
521
|
+
}
|
|
522
|
+
/** Exposed for inspection/tests — returns the current sessionId (or null if not in a session). */
|
|
523
|
+
get currentSessionId() {
|
|
524
|
+
return this.session?.sessionId ?? null;
|
|
525
|
+
}
|
|
526
|
+
// ---------- sessions ----------
|
|
527
|
+
installSessionTracking() {
|
|
528
|
+
this.session = this.startNewSession();
|
|
529
|
+
this.emitSessionStart();
|
|
530
|
+
const onVisChange = () => {
|
|
531
|
+
if (!this.session) return;
|
|
532
|
+
const doc2 = globalThis.document;
|
|
533
|
+
if (doc2.visibilityState === "hidden") {
|
|
534
|
+
this.session.hiddenAt = Date.now();
|
|
535
|
+
} else if (doc2.visibilityState === "visible") {
|
|
536
|
+
const hiddenFor = this.session.hiddenAt ? Date.now() - this.session.hiddenAt : 0;
|
|
537
|
+
if (hiddenFor >= SESSION_RESUME_THRESHOLD_MS) {
|
|
538
|
+
this.emitSessionEnd();
|
|
539
|
+
this.session = this.startNewSession();
|
|
540
|
+
this.emitSessionStart();
|
|
541
|
+
} else {
|
|
542
|
+
this.session.hiddenAt = null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
const onPageHide = () => this.emitSessionEnd();
|
|
547
|
+
const w = globalThis.window;
|
|
548
|
+
const doc = globalThis.document;
|
|
549
|
+
doc.addEventListener("visibilitychange", onVisChange);
|
|
550
|
+
w.addEventListener("pagehide", onPageHide);
|
|
551
|
+
w.addEventListener("beforeunload", onPageHide);
|
|
552
|
+
this.cleanups.push(() => {
|
|
553
|
+
doc.removeEventListener("visibilitychange", onVisChange);
|
|
554
|
+
w.removeEventListener("pagehide", onPageHide);
|
|
555
|
+
w.removeEventListener("beforeunload", onPageHide);
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
startNewSession() {
|
|
559
|
+
return {
|
|
560
|
+
sessionId: mintSessionId(),
|
|
561
|
+
startedAt: Date.now(),
|
|
562
|
+
hiddenAt: null,
|
|
563
|
+
endedSent: false
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
emitSessionStart() {
|
|
567
|
+
if (!this.session) return;
|
|
568
|
+
this.track("session.started", { sessionId: this.session.sessionId });
|
|
569
|
+
}
|
|
570
|
+
emitSessionEnd() {
|
|
571
|
+
if (!this.session || this.session.endedSent) return;
|
|
572
|
+
const duration = Date.now() - this.session.startedAt;
|
|
573
|
+
this.track("session.ended", {
|
|
574
|
+
sessionId: this.session.sessionId,
|
|
575
|
+
durationMs: duration
|
|
576
|
+
});
|
|
577
|
+
this.session.endedSent = true;
|
|
578
|
+
}
|
|
579
|
+
// ---------- page views ----------
|
|
580
|
+
installPageViewTracking() {
|
|
581
|
+
const w = globalThis.window;
|
|
582
|
+
const doc = globalThis.document;
|
|
583
|
+
const fire = () => {
|
|
584
|
+
const loc = w.location;
|
|
585
|
+
this.track("page.viewed", {
|
|
586
|
+
path: loc.pathname,
|
|
587
|
+
url: loc.href,
|
|
588
|
+
search: loc.search || void 0,
|
|
589
|
+
hash: loc.hash || void 0,
|
|
590
|
+
title: doc.title,
|
|
591
|
+
// referrer only on the first hit of the session — afterward it's
|
|
592
|
+
// always our previous URL, which isn't useful.
|
|
593
|
+
referrer: doc.referrer || void 0
|
|
594
|
+
});
|
|
595
|
+
};
|
|
596
|
+
fire();
|
|
597
|
+
const origPush = w.history.pushState;
|
|
598
|
+
const origReplace = w.history.replaceState;
|
|
599
|
+
function patchedPush(data, unused, url) {
|
|
600
|
+
origPush.apply(this, [data, unused, url]);
|
|
601
|
+
queueMicrotask(fire);
|
|
602
|
+
}
|
|
603
|
+
function patchedReplace(data, unused, url) {
|
|
604
|
+
origReplace.apply(this, [data, unused, url]);
|
|
605
|
+
queueMicrotask(fire);
|
|
606
|
+
}
|
|
607
|
+
w.history.pushState = patchedPush;
|
|
608
|
+
w.history.replaceState = patchedReplace;
|
|
609
|
+
const onPopState = () => fire();
|
|
610
|
+
w.addEventListener("popstate", onPopState);
|
|
611
|
+
this.cleanups.push(() => {
|
|
612
|
+
if (w.history.pushState === patchedPush) {
|
|
613
|
+
w.history.pushState = origPush;
|
|
614
|
+
}
|
|
615
|
+
if (w.history.replaceState === patchedReplace) {
|
|
616
|
+
w.history.replaceState = origReplace;
|
|
617
|
+
}
|
|
618
|
+
w.removeEventListener("popstate", onPopState);
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
function isBrowserSafe() {
|
|
623
|
+
return typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
|
|
624
|
+
}
|
|
625
|
+
function mintSessionId() {
|
|
626
|
+
const ts = Date.now().toString(36);
|
|
627
|
+
return `sess_${ts}${randomChars(10)}`;
|
|
628
|
+
}
|
|
629
|
+
|
|
392
630
|
// src/crossdeck.ts
|
|
393
631
|
var CrossdeckClient = class {
|
|
394
632
|
constructor() {
|
|
@@ -409,6 +647,7 @@ var CrossdeckClient = class {
|
|
|
409
647
|
}
|
|
410
648
|
const storage = options.storage ?? detectDefaultStorage();
|
|
411
649
|
const persistIdentity = options.persistIdentity ?? true;
|
|
650
|
+
const autoTrack = resolveAutoTrack(options.autoTrack);
|
|
412
651
|
const opts = {
|
|
413
652
|
publicKey: options.publicKey,
|
|
414
653
|
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
@@ -417,7 +656,9 @@ var CrossdeckClient = class {
|
|
|
417
656
|
autoHeartbeat: options.autoHeartbeat ?? true,
|
|
418
657
|
eventFlushBatchSize: options.eventFlushBatchSize ?? 20,
|
|
419
658
|
eventFlushIntervalMs: options.eventFlushIntervalMs ?? 5e3,
|
|
420
|
-
sdkVersion: options.sdkVersion ?? SDK_VERSION
|
|
659
|
+
sdkVersion: options.sdkVersion ?? SDK_VERSION,
|
|
660
|
+
autoTrack,
|
|
661
|
+
appVersion: options.appVersion ?? null
|
|
421
662
|
};
|
|
422
663
|
const http = new HttpClient({
|
|
423
664
|
publicKey: opts.publicKey,
|
|
@@ -432,14 +673,25 @@ var CrossdeckClient = class {
|
|
|
432
673
|
batchSize: opts.eventFlushBatchSize,
|
|
433
674
|
intervalMs: opts.eventFlushIntervalMs
|
|
434
675
|
});
|
|
676
|
+
const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
|
|
435
677
|
this.state = {
|
|
436
678
|
http,
|
|
437
679
|
identity,
|
|
438
680
|
entitlements,
|
|
439
681
|
events,
|
|
682
|
+
autoTracker: null,
|
|
683
|
+
deviceInfo,
|
|
440
684
|
options: opts,
|
|
441
685
|
developerUserId: null
|
|
442
686
|
};
|
|
687
|
+
if (autoTrack.sessions || autoTrack.pageViews) {
|
|
688
|
+
const tracker = new AutoTracker(
|
|
689
|
+
autoTrack,
|
|
690
|
+
(name, properties) => this.track(name, properties)
|
|
691
|
+
);
|
|
692
|
+
this.state.autoTracker = tracker;
|
|
693
|
+
tracker.install();
|
|
694
|
+
}
|
|
443
695
|
if (opts.autoHeartbeat) {
|
|
444
696
|
void this.heartbeat().catch(() => void 0);
|
|
445
697
|
}
|
|
@@ -510,11 +762,15 @@ var CrossdeckClient = class {
|
|
|
510
762
|
message: "track(name) requires a non-empty name."
|
|
511
763
|
});
|
|
512
764
|
}
|
|
765
|
+
const enriched = { ...s.deviceInfo };
|
|
766
|
+
const sessionId = s.autoTracker?.currentSessionId;
|
|
767
|
+
if (sessionId) enriched.sessionId = sessionId;
|
|
768
|
+
if (properties) Object.assign(enriched, properties);
|
|
513
769
|
const event = {
|
|
514
770
|
eventId: this.mintEventId(),
|
|
515
771
|
name,
|
|
516
772
|
timestamp: Date.now(),
|
|
517
|
-
properties:
|
|
773
|
+
properties: enriched
|
|
518
774
|
};
|
|
519
775
|
Object.assign(event, this.identityHintForEvent());
|
|
520
776
|
s.events.enqueue(event);
|
|
@@ -556,10 +812,19 @@ var CrossdeckClient = class {
|
|
|
556
812
|
*/
|
|
557
813
|
reset() {
|
|
558
814
|
if (!this.state) return;
|
|
815
|
+
this.state.autoTracker?.uninstall();
|
|
559
816
|
this.state.identity.reset();
|
|
560
817
|
this.state.entitlements.clear();
|
|
561
818
|
this.state.events.reset();
|
|
562
819
|
this.state.developerUserId = null;
|
|
820
|
+
if (this.state.autoTracker) {
|
|
821
|
+
const tracker = new AutoTracker(
|
|
822
|
+
this.state.options.autoTrack,
|
|
823
|
+
(name, props) => this.track(name, props)
|
|
824
|
+
);
|
|
825
|
+
this.state.autoTracker = tracker;
|
|
826
|
+
tracker.install();
|
|
827
|
+
}
|
|
563
828
|
}
|
|
564
829
|
/**
|
|
565
830
|
* Diagnostic: current state + queue stats. Useful for the dashboard's
|
|
@@ -642,6 +907,19 @@ var CrossdeckClient = class {
|
|
|
642
907
|
}
|
|
643
908
|
};
|
|
644
909
|
var Crossdeck = new CrossdeckClient();
|
|
910
|
+
function resolveAutoTrack(input) {
|
|
911
|
+
if (input === false) {
|
|
912
|
+
return { sessions: false, pageViews: false, deviceInfo: false };
|
|
913
|
+
}
|
|
914
|
+
if (input === void 0 || input === true) {
|
|
915
|
+
return { ...DEFAULT_AUTO_TRACK };
|
|
916
|
+
}
|
|
917
|
+
return {
|
|
918
|
+
sessions: input.sessions ?? DEFAULT_AUTO_TRACK.sessions,
|
|
919
|
+
pageViews: input.pageViews ?? DEFAULT_AUTO_TRACK.pageViews,
|
|
920
|
+
deviceInfo: input.deviceInfo ?? DEFAULT_AUTO_TRACK.deviceInfo
|
|
921
|
+
};
|
|
922
|
+
}
|
|
645
923
|
// Annotate the CommonJS export names for ESM import in node:
|
|
646
924
|
0 && (module.exports = {
|
|
647
925
|
Crossdeck,
|