@nexly/core 0.8.1 → 0.9.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/collect.d.ts CHANGED
@@ -1,13 +1,23 @@
1
+ /**
2
+ * Platform-agnostic event envelope types and payload builder.
3
+ *
4
+ * This module is safe to import from any runtime (browser, React Native, Node).
5
+ * Transport (beacon / fetch / native) lives in platform-specific packages.
6
+ */
1
7
  /** Auth fields for ingest (no transport URL). */
2
8
  export type IngestCredentials = {
3
9
  appId: string;
4
10
  apiToken: string;
5
11
  };
12
+ /** Flat credentials + URL for small apps and event helpers. */
13
+ export type CollectCredentials = {
14
+ collectUrl: string;
15
+ } & IngestCredentials;
6
16
  /**
7
17
  * Event envelope:
8
- * - `context` — per-event volatile metadata (url, pathname, viewport, etc.).
9
- * - `data` — owner-defined custom properties (later: only keys registered for the app).
10
- * - `session_context` — session-stable metadata (UA, screen, language), sent once per session.
18
+ * - `context` — per-event volatile metadata (path, screen, viewport, visitor_id, session_id, …).
19
+ * - `data` — owner-defined custom properties.
20
+ * - `session_context` — session-stable metadata (OS, locale, screen), sent once per session.
11
21
  */
12
22
  export type TrackEventInput = {
13
23
  credentials: IngestCredentials;
@@ -17,22 +27,10 @@ export type TrackEventInput = {
17
27
  data?: Record<string, unknown>;
18
28
  sessionContext?: Record<string, unknown>;
19
29
  };
20
- /** Full send: transport URL + event envelope. */
21
- export type SendBeaconCollectInput = TrackEventInput & {
22
- collectUrl: string;
23
- };
24
- /** Flat credentials + URL for small apps and event helpers (maps to {@link TrackEventInput.credentials}). */
25
- export type CollectCredentials = {
26
- collectUrl: string;
27
- } & IngestCredentials;
28
30
  /**
29
31
  * Builds the JSON body for POST /v1/collect (snake_case wire format).
30
- * Duplicates `context.path` to top-level `path` when present.
32
+ * Duplicates `context.path` to the top-level `path` when present.
31
33
  * Includes `session_context` when provided (first event of a session).
32
34
  */
33
35
  export declare function buildCollectPayload(input: TrackEventInput): Record<string, unknown>;
34
- /**
35
- * Packages the payload and sends it via `navigator.sendBeacon` (survives page unload).
36
- */
37
- export declare function sendBeaconCollect(input: SendBeaconCollectInput): boolean;
38
36
  //# sourceMappingURL=collect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../src/collect.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,EAAE,iBAAiB,CAAA;IAC9B,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACzC,CAAA;AAED,iDAAiD;AACjD,MAAM,MAAM,sBAAsB,GAAG,eAAe,GAAG;IACrD,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,6GAA6G;AAC7G,MAAM,MAAM,kBAAkB,GAAG;IAC/B,UAAU,EAAE,MAAM,CAAA;CACnB,GAAG,iBAAiB,CAAA;AAMrB;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAqBnF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CASxE"}
1
+ {"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../src/collect.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,iDAAiD;AACjD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,+DAA+D;AAC/D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,UAAU,EAAE,MAAM,CAAA;CACnB,GAAG,iBAAiB,CAAA;AAErB;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,EAAE,iBAAiB,CAAA;IAC9B,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACzC,CAAA;AAMD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAoBnF"}
package/dist/collect.js CHANGED
@@ -1,16 +1,21 @@
1
+ /**
2
+ * Platform-agnostic event envelope types and payload builder.
3
+ *
4
+ * This module is safe to import from any runtime (browser, React Native, Node).
5
+ * Transport (beacon / fetch / native) lives in platform-specific packages.
6
+ */
1
7
  function isPlainObject(v) {
2
8
  return v !== null && typeof v === 'object' && !Array.isArray(v);
3
9
  }
4
10
  /**
5
11
  * Builds the JSON body for POST /v1/collect (snake_case wire format).
6
- * Duplicates `context.path` to top-level `path` when present.
12
+ * Duplicates `context.path` to the top-level `path` when present.
7
13
  * Includes `session_context` when provided (first event of a session).
8
14
  */
9
15
  export function buildCollectPayload(input) {
10
16
  const context = isPlainObject(input.context) ? input.context : {};
11
17
  const data = isPlainObject(input.data) ? input.data : {};
12
18
  const pathFromContext = typeof context.path === 'string' ? context.path : '';
13
- const path = pathFromContext;
14
19
  const body = {
15
20
  app_id: input.credentials.appId,
16
21
  api_token: input.credentials.apiToken,
@@ -19,8 +24,8 @@ export function buildCollectPayload(input) {
19
24
  context,
20
25
  data,
21
26
  };
22
- if (path) {
23
- body.path = path;
27
+ if (pathFromContext) {
28
+ body.path = pathFromContext;
24
29
  }
25
30
  const sc = isPlainObject(input.sessionContext) ? input.sessionContext : null;
26
31
  if (sc && Object.keys(sc).length > 0) {
@@ -28,16 +33,3 @@ export function buildCollectPayload(input) {
28
33
  }
29
34
  return body;
30
35
  }
31
- /**
32
- * Packages the payload and sends it via `navigator.sendBeacon` (survives page unload).
33
- */
34
- export function sendBeaconCollect(input) {
35
- const url = (input.collectUrl ?? '').trim();
36
- if (!url) {
37
- return false;
38
- }
39
- const payload = buildCollectPayload(input);
40
- const body = JSON.stringify(payload);
41
- const blob = new Blob([body], { type: 'application/json' });
42
- return navigator.sendBeacon(url, blob);
43
- }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Browser event ingest client (public API).
2
+ * Platform-agnostic Nexly ingest core: types, payload builder, and the {@link NexlyBase} abstract class.
3
+ *
4
+ * Platform-specific transports and metadata live in sibling packages:
5
+ * - `@nexly/web` — browser (sendBeacon, DOM meta, engagement listeners).
6
+ * - `@nexly/react-native` — React Native (fetch, AsyncStorage, AppState).
3
7
  */
4
- export { Nexly, type NexlyEventInput, type NexlyEventType, type NexlyInit } from './nexly.js';
5
- export { collectBrowserMeta, collectSessionMeta, collectEventMeta } from './browser-meta.js';
6
- export { getSessionId, getVisitorId } from './session.js';
7
- export { buildCollectPayload, sendBeaconCollect, type CollectCredentials, type IngestCredentials, type SendBeaconCollectInput, type TrackEventInput, } from './collect.js';
8
- export { getDefaultPathname, sendPageViewBeacon, startEngagementTracking } from './events/index.js';
8
+ export { buildCollectPayload, type CollectCredentials, type IngestCredentials, type TrackEventInput, } from './collect.js';
9
+ export { NexlyBase, DEFAULT_COLLECT_URL, type NexlyEventInput, type NexlyEventType, type NexlyInit, } from './nexly-base.js';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,KAAK,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAC7F,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAC5F,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,eAAe,GACrB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,mBAAmB,EACnB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,eAAe,GACrB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,SAAS,GACf,MAAM,iBAAiB,CAAA"}
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Browser event ingest client (public API).
2
+ * Platform-agnostic Nexly ingest core: types, payload builder, and the {@link NexlyBase} abstract class.
3
+ *
4
+ * Platform-specific transports and metadata live in sibling packages:
5
+ * - `@nexly/web` — browser (sendBeacon, DOM meta, engagement listeners).
6
+ * - `@nexly/react-native` — React Native (fetch, AsyncStorage, AppState).
3
7
  */
4
- export { Nexly } from './nexly.js';
5
- export { collectBrowserMeta, collectSessionMeta, collectEventMeta } from './browser-meta.js';
6
- export { getSessionId, getVisitorId } from './session.js';
7
- export { buildCollectPayload, sendBeaconCollect, } from './collect.js';
8
- export { getDefaultPathname, sendPageViewBeacon, startEngagementTracking } from './events/index.js';
8
+ export { buildCollectPayload, } from './collect.js';
9
+ export { NexlyBase, DEFAULT_COLLECT_URL, } from './nexly-base.js';
@@ -0,0 +1,60 @@
1
+ import { type IngestCredentials } from './collect.js';
2
+ export declare const DEFAULT_COLLECT_URL = "https://gate.nexly.to/v1/collect";
3
+ /** Options for {@link NexlyBase}. `key` is the ingest API token (sent as `api_token` on the wire). */
4
+ export type NexlyInit = {
5
+ /** Override the ingest endpoint. Defaults to the Nexly production gateway. */
6
+ collectUrl?: string;
7
+ appId: string;
8
+ key: string;
9
+ };
10
+ /** Allowed event types. The backend rejects any other value. */
11
+ export type NexlyEventType = 'pageview' | 'engagement' | 'custom';
12
+ export type NexlyEventInput = {
13
+ name: string;
14
+ type: NexlyEventType | string;
15
+ data?: Record<string, unknown>;
16
+ /** Merged on top of default platform context (see {@link NexlyBase.collectEventContext}). */
17
+ context?: Record<string, unknown>;
18
+ };
19
+ /**
20
+ * Platform-agnostic base class: session state, payload assembly, disabled-state logic.
21
+ *
22
+ * Subclasses implement:
23
+ * - {@link transport} — how bytes reach the collector (beacon / fetch / native bridge).
24
+ * - {@link collectEventContext} — per-event volatile metadata (path, visitor/session IDs, viewport).
25
+ * - {@link collectSessionContext} — session-stable metadata (OS, locale, screen).
26
+ * - {@link getDefaultPath} — current path / screen name.
27
+ * - {@link startEngagement} — platform-specific engagement listeners. Returns a stop function.
28
+ */
29
+ export declare abstract class NexlyBase {
30
+ private static _instance;
31
+ /** Shared singleton access. Each concrete class's `init()` should write here. */
32
+ static getInstance(): NexlyBase | null;
33
+ protected static setInstance(instance: NexlyBase | null): void;
34
+ readonly collectUrl: string;
35
+ readonly appId: string;
36
+ readonly key: string;
37
+ protected constructor(init: NexlyInit);
38
+ protected get disabled(): boolean;
39
+ protected get credentials(): IngestCredentials;
40
+ /** Platform-specific byte transport. Must be fire-and-forget (return quickly). */
41
+ protected abstract transport(payload: Record<string, unknown>): boolean;
42
+ /** Per-event volatile metadata merged into `context`. */
43
+ protected abstract collectEventContext(): Record<string, unknown>;
44
+ /** Session-stable metadata sent as `session_context`. */
45
+ protected abstract collectSessionContext(): Record<string, unknown>;
46
+ /** Current path (web) or screen name (native). */
47
+ abstract getDefaultPath(): string;
48
+ /** Start platform-specific engagement tracking. Returns a cleanup function. */
49
+ abstract startEngagement(): () => void;
50
+ /**
51
+ * Sends a page view (`event_name` / `event_type`: `pageview`) with full context.
52
+ */
53
+ pageview(path?: string): boolean;
54
+ /**
55
+ * Sends a custom event. Pass `context` to merge or override default fields.
56
+ */
57
+ event(input: NexlyEventInput): boolean;
58
+ private dispatch;
59
+ }
60
+ //# sourceMappingURL=nexly-base.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nexly-base.d.ts","sourceRoot":"","sources":["../src/nexly-base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAI1E,eAAO,MAAM,mBAAmB,qCAAqC,CAAA;AAErE,sGAAsG;AACtG,MAAM,MAAM,SAAS,GAAG;IACtB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,gEAAgE;AAChE,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,YAAY,GAAG,QAAQ,CAAA;AAEjE,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,cAAc,GAAG,MAAM,CAAA;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,6FAA6F;IAC7F,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClC,CAAA;AAED;;;;;;;;;GASG;AACH,8BAAsB,SAAS;IAC7B,OAAO,CAAC,MAAM,CAAC,SAAS,CAAyB;IAEjD,iFAAiF;IACjF,MAAM,CAAC,WAAW,IAAI,SAAS,GAAG,IAAI;IAItC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,SAAS,GAAG,IAAI,GAAG,IAAI;IAI9D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IAEpB,SAAS,aAAa,IAAI,EAAE,SAAS;IAQrC,SAAS,KAAK,QAAQ,IAAI,OAAO,CAEhC;IAED,SAAS,KAAK,WAAW,IAAI,iBAAiB,CAE7C;IAED,kFAAkF;IAClF,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO;IAEvE,yDAAyD;IACzD,SAAS,CAAC,QAAQ,CAAC,mBAAmB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAEjE,yDAAyD;IACzD,SAAS,CAAC,QAAQ,CAAC,qBAAqB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAEnE,kDAAkD;IAClD,QAAQ,CAAC,cAAc,IAAI,MAAM;IAEjC,+EAA+E;IAC/E,QAAQ,CAAC,eAAe,IAAI,MAAM,IAAI;IAEtC;;OAEG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO;IAYhC;;OAEG;IACH,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO;IAatC,OAAO,CAAC,QAAQ;CAiBjB"}
@@ -0,0 +1,82 @@
1
+ import { buildCollectPayload } from './collect.js';
2
+ export const DEFAULT_COLLECT_URL = 'https://gate.nexly.to/v1/collect';
3
+ /**
4
+ * Platform-agnostic base class: session state, payload assembly, disabled-state logic.
5
+ *
6
+ * Subclasses implement:
7
+ * - {@link transport} — how bytes reach the collector (beacon / fetch / native bridge).
8
+ * - {@link collectEventContext} — per-event volatile metadata (path, visitor/session IDs, viewport).
9
+ * - {@link collectSessionContext} — session-stable metadata (OS, locale, screen).
10
+ * - {@link getDefaultPath} — current path / screen name.
11
+ * - {@link startEngagement} — platform-specific engagement listeners. Returns a stop function.
12
+ */
13
+ export class NexlyBase {
14
+ static _instance = null;
15
+ /** Shared singleton access. Each concrete class's `init()` should write here. */
16
+ static getInstance() {
17
+ return NexlyBase._instance;
18
+ }
19
+ static setInstance(instance) {
20
+ NexlyBase._instance = instance;
21
+ }
22
+ collectUrl;
23
+ appId;
24
+ key;
25
+ constructor(init) {
26
+ this.collectUrl = (init.collectUrl ?? DEFAULT_COLLECT_URL).trim();
27
+ this.appId = (init.appId ?? '').trim();
28
+ this.key = (init.key ?? '').trim();
29
+ if (!this.appId)
30
+ console.warn('[Nexly] Missing appId — tracking will be disabled.');
31
+ if (!this.key)
32
+ console.warn('[Nexly] Missing key — tracking will be disabled.');
33
+ }
34
+ get disabled() {
35
+ return !this.appId || !this.key;
36
+ }
37
+ get credentials() {
38
+ return { appId: this.appId, apiToken: this.key };
39
+ }
40
+ /**
41
+ * Sends a page view (`event_name` / `event_type`: `pageview`) with full context.
42
+ */
43
+ pageview(path) {
44
+ if (this.disabled)
45
+ return false;
46
+ const resolvedPath = path ?? this.getDefaultPath();
47
+ return this.dispatch({
48
+ eventName: 'pageview',
49
+ eventType: 'pageview',
50
+ context: { ...this.collectEventContext(), path: resolvedPath },
51
+ data: {},
52
+ sessionContext: this.collectSessionContext(),
53
+ });
54
+ }
55
+ /**
56
+ * Sends a custom event. Pass `context` to merge or override default fields.
57
+ */
58
+ event(input) {
59
+ if (this.disabled)
60
+ return false;
61
+ const defaultContext = { ...this.collectEventContext(), path: this.getDefaultPath() };
62
+ const context = input.context ? { ...defaultContext, ...input.context } : defaultContext;
63
+ return this.dispatch({
64
+ eventName: input.name,
65
+ eventType: input.type,
66
+ context,
67
+ data: input.data ?? {},
68
+ sessionContext: this.collectSessionContext(),
69
+ });
70
+ }
71
+ dispatch(args) {
72
+ const payload = buildCollectPayload({
73
+ credentials: this.credentials,
74
+ eventName: args.eventName,
75
+ eventType: args.eventType,
76
+ context: args.context,
77
+ data: args.data,
78
+ sessionContext: args.sessionContext,
79
+ });
80
+ return this.transport(payload);
81
+ }
82
+ }
package/package.json CHANGED
@@ -1,14 +1,12 @@
1
1
  {
2
2
  "name": "@nexly/core",
3
- "version": "0.8.1",
4
- "description": "Nexly browser ingest core (sendBeacon, Nexly client, context, engagement)",
3
+ "version": "0.9.0",
4
+ "description": "Nexly ingest core (platform-agnostic): types, payload builder, abstract NexlyBase client",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "nexly",
8
8
  "analytics",
9
9
  "tracking",
10
- "sendBeacon",
11
- "browser",
12
10
  "ingest"
13
11
  ],
14
12
  "type": "module",
@@ -1,26 +0,0 @@
1
- /**
2
- * Browser metadata collection, split into session-stable and per-event fields.
3
- *
4
- * - `collectSessionMeta()` — stable within a session (UA, screen, language, etc.), sent once.
5
- * - `collectEventMeta()` — per-event volatile fields (url, pathname, referrer), sent with every event.
6
- * - `collectBrowserMeta()` — legacy: merges both for backward compatibility.
7
- *
8
- * Does not include geo (country/region/city) — that requires server-side IP lookup.
9
- */
10
- /**
11
- * Session-stable metadata: user agent, screen, language, timezone, etc.
12
- * Collected once per session and sent as `session_context` on the wire.
13
- */
14
- export declare function collectSessionMeta(): Record<string, unknown>;
15
- /**
16
- * Per-event volatile metadata: only fields that actually change between events.
17
- * Includes `visitor_id` and `session_id` for server-side extraction,
18
- * plus `url` and `pathname` to track SPA navigation.
19
- */
20
- export declare function collectEventMeta(): Record<string, unknown>;
21
- /**
22
- * Legacy: full browser metadata (session + event fields merged).
23
- * Kept for backward compatibility with `Nexly.event()` and custom events.
24
- */
25
- export declare function collectBrowserMeta(): Record<string, unknown>;
26
- //# sourceMappingURL=browser-meta.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"browser-meta.d.ts","sourceRoot":"","sources":["../src/browser-meta.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAkBH;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAiF5D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAc1D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE5D"}
@@ -1,119 +0,0 @@
1
- /**
2
- * Browser metadata collection, split into session-stable and per-event fields.
3
- *
4
- * - `collectSessionMeta()` — stable within a session (UA, screen, language, etc.), sent once.
5
- * - `collectEventMeta()` — per-event volatile fields (url, pathname, referrer), sent with every event.
6
- * - `collectBrowserMeta()` — legacy: merges both for backward compatibility.
7
- *
8
- * Does not include geo (country/region/city) — that requires server-side IP lookup.
9
- */
10
- import { getSessionId, getVisitorId } from './session.js';
11
- function safe(fn) {
12
- try {
13
- return fn();
14
- }
15
- catch {
16
- return undefined;
17
- }
18
- }
19
- function filterEmpty(obj) {
20
- return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined && v !== '' && !(Array.isArray(v) && v.length === 0)));
21
- }
22
- /**
23
- * Session-stable metadata: user agent, screen, language, timezone, etc.
24
- * Collected once per session and sent as `session_context` on the wire.
25
- */
26
- export function collectSessionMeta() {
27
- if (typeof window === 'undefined' || typeof navigator === 'undefined') {
28
- return {};
29
- }
30
- const out = {};
31
- out.language = safe(() => navigator.language);
32
- const langs = safe(() => navigator.languages);
33
- if (langs?.length) {
34
- out.languages = [...langs];
35
- }
36
- out.timezone = safe(() => Intl.DateTimeFormat().resolvedOptions().timeZone);
37
- out.screen_width = safe(() => window.screen.width);
38
- out.screen_height = safe(() => window.screen.height);
39
- out.avail_screen_width = safe(() => window.screen.availWidth);
40
- out.avail_screen_height = safe(() => window.screen.availHeight);
41
- out.color_depth = safe(() => window.screen.colorDepth);
42
- out.viewport_width = safe(() => window.innerWidth);
43
- out.viewport_height = safe(() => window.innerHeight);
44
- out.device_pixel_ratio = safe(() => window.devicePixelRatio);
45
- const ref = safe(() => document.referrer);
46
- if (ref) {
47
- out.referrer = ref;
48
- }
49
- out.online = safe(() => navigator.onLine);
50
- out.user_agent = safe(() => navigator.userAgent);
51
- out.navigator_platform = safe(() => navigator.platform);
52
- out.cookie_enabled = safe(() => navigator.cookieEnabled);
53
- out.hardware_concurrency = safe(() => navigator.hardwareConcurrency);
54
- out.max_touch_points = safe(() => navigator.maxTouchPoints);
55
- const conn = safe(() => navigator.connection);
56
- if (conn) {
57
- const downlink = safe(() => conn.downlink);
58
- const effectiveType = safe(() => conn.effectiveType);
59
- const rtt = safe(() => conn.rtt);
60
- if (downlink !== undefined) {
61
- out.network_downlink_mbps = downlink;
62
- }
63
- if (effectiveType !== undefined) {
64
- out.network_effective_type = effectiveType;
65
- }
66
- if (rtt !== undefined) {
67
- out.network_rtt_ms = rtt;
68
- }
69
- }
70
- const ud = safe(() => navigator.userAgentData);
71
- if (ud) {
72
- const mobile = safe(() => ud.mobile);
73
- const platform = safe(() => ud.platform);
74
- if (mobile !== undefined) {
75
- out.ua_mobile = mobile;
76
- }
77
- if (platform) {
78
- out.ua_platform = platform;
79
- }
80
- const brands = safe(() => ud.brands);
81
- if (brands?.length) {
82
- out.ua_brands = brands.map((b) => `${b.brand} ${b.version}`).join(', ');
83
- }
84
- }
85
- if (typeof window.matchMedia === 'function') {
86
- const dark = safe(() => window.matchMedia('(prefers-color-scheme: dark)').matches);
87
- if (dark !== undefined) {
88
- out.prefers_color_scheme = dark ? 'dark' : 'light';
89
- }
90
- const reduced = safe(() => window.matchMedia('(prefers-reduced-motion: reduce)').matches);
91
- if (reduced !== undefined) {
92
- out.prefers_reduced_motion = reduced;
93
- }
94
- }
95
- return filterEmpty(out);
96
- }
97
- /**
98
- * Per-event volatile metadata: only fields that actually change between events.
99
- * Includes `visitor_id` and `session_id` for server-side extraction,
100
- * plus `url` and `pathname` to track SPA navigation.
101
- */
102
- export function collectEventMeta() {
103
- if (typeof window === 'undefined' || typeof navigator === 'undefined') {
104
- return {};
105
- }
106
- const out = {};
107
- out.visitor_id = safe(() => getVisitorId());
108
- out.session_id = safe(() => getSessionId());
109
- out.url = safe(() => window.location.href);
110
- out.pathname = safe(() => window.location.pathname);
111
- return filterEmpty(out);
112
- }
113
- /**
114
- * Legacy: full browser metadata (session + event fields merged).
115
- * Kept for backward compatibility with `Nexly.event()` and custom events.
116
- */
117
- export function collectBrowserMeta() {
118
- return { ...collectSessionMeta(), ...collectEventMeta() };
119
- }
@@ -1,13 +0,0 @@
1
- import { type CollectCredentials } from '../collect.js';
2
- type StopFn = () => void;
3
- /**
4
- * Attaches engagement listeners (scroll, click, input focus, visibility, keepalive heartbeat).
5
- * Returns a cleanup function that removes all listeners and timers.
6
- *
7
- * Duration is tracked client-side: a 1s tick increments `activeSeconds` only while visible.
8
- * A lightweight keepalive heartbeat fires every 60s for the realtime dashboard.
9
- * Summary events are sent on visibility hidden and beforeunload.
10
- */
11
- export declare function startEngagementTracking(creds: CollectCredentials): StopFn;
12
- export {};
13
- //# sourceMappingURL=engagement.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"engagement.d.ts","sourceRoot":"","sources":["../../src/events/engagement.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAyB1E,KAAK,MAAM,GAAG,MAAM,IAAI,CAAA;AAExB;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,CAmIzE"}
@@ -1,143 +0,0 @@
1
- import { collectEventMeta, collectSessionMeta } from '../browser-meta.js';
2
- import { sendBeaconCollect } from '../collect.js';
3
- const CLICK_THROTTLE_MS = 500;
4
- const KEEPALIVE_INTERVAL_MS = 60_000;
5
- const SCROLL_DEBOUNCE_MS = 300;
6
- const ACTIVE_TICK_MS = 1_000;
7
- function sendEngagement(creds, eventName, eventType, data, sessionContext) {
8
- sendBeaconCollect({
9
- collectUrl: creds.collectUrl,
10
- credentials: { appId: creds.appId, apiToken: creds.apiToken },
11
- eventName,
12
- eventType,
13
- context: collectEventMeta(),
14
- data,
15
- sessionContext,
16
- });
17
- }
18
- /**
19
- * Attaches engagement listeners (scroll, click, input focus, visibility, keepalive heartbeat).
20
- * Returns a cleanup function that removes all listeners and timers.
21
- *
22
- * Duration is tracked client-side: a 1s tick increments `activeSeconds` only while visible.
23
- * A lightweight keepalive heartbeat fires every 60s for the realtime dashboard.
24
- * Summary events are sent on visibility hidden and beforeunload.
25
- */
26
- export function startEngagementTracking(creds) {
27
- if (typeof window === 'undefined' || typeof document === 'undefined') {
28
- return () => { };
29
- }
30
- const ac = new AbortController();
31
- const { signal } = ac;
32
- let sessionContextSent = false;
33
- function getSessionContextOnce() {
34
- if (sessionContextSent)
35
- return undefined;
36
- sessionContextSent = true;
37
- return collectSessionMeta();
38
- }
39
- // ---- client-side active duration tracking ----
40
- let activeSeconds = 0;
41
- const activeTickId = setInterval(() => {
42
- if (document.visibilityState === 'visible') {
43
- activeSeconds++;
44
- }
45
- }, ACTIVE_TICK_MS);
46
- // ---- scroll depth ----
47
- let maxScrollPercent = 0;
48
- let scrollTimer = null;
49
- function updateScrollPercent() {
50
- const docH = document.documentElement.scrollHeight;
51
- const viewH = window.innerHeight;
52
- if (docH <= viewH) {
53
- maxScrollPercent = 100;
54
- return;
55
- }
56
- const pct = Math.round((window.scrollY / (docH - viewH)) * 100);
57
- if (pct > maxScrollPercent) {
58
- maxScrollPercent = pct;
59
- }
60
- }
61
- function onScroll() {
62
- if (scrollTimer)
63
- clearTimeout(scrollTimer);
64
- scrollTimer = setTimeout(updateScrollPercent, SCROLL_DEBOUNCE_MS);
65
- }
66
- window.addEventListener('scroll', onScroll, { passive: true, signal });
67
- function flushScrollDepth() {
68
- updateScrollPercent();
69
- if (maxScrollPercent > 0) {
70
- sendEngagement(creds, 'scroll_depth', 'engagement', { max_percent: maxScrollPercent });
71
- }
72
- }
73
- // ---- click ----
74
- let lastClickTs = 0;
75
- function onClick(e) {
76
- const now = Date.now();
77
- if (now - lastClickTs < CLICK_THROTTLE_MS)
78
- return;
79
- lastClickTs = now;
80
- const el = e.target;
81
- if (!el)
82
- return;
83
- const tag = el.tagName?.toLowerCase() ?? '';
84
- const rawText = el.innerText ?? '';
85
- const text = rawText.slice(0, 80).trim();
86
- sendEngagement(creds, 'click', 'engagement', {
87
- tag,
88
- ...(text ? { text } : {}),
89
- });
90
- }
91
- document.addEventListener('click', onClick, { signal });
92
- // ---- input focus (once per element per session) ----
93
- const focusedElements = new WeakSet();
94
- function onFocusIn(e) {
95
- const el = e.target;
96
- if (!el)
97
- return;
98
- const tag = el.tagName?.toLowerCase() ?? '';
99
- if (tag !== 'input' && tag !== 'textarea' && tag !== 'select')
100
- return;
101
- if (focusedElements.has(el))
102
- return;
103
- focusedElements.add(el);
104
- sendEngagement(creds, 'input_focus', 'engagement', {
105
- tag,
106
- ...(el.name ? { name: el.name } : {}),
107
- ...(el.type ? { type: el.type } : {}),
108
- });
109
- }
110
- document.addEventListener('focusin', onFocusIn, { signal });
111
- // ---- visibility / session summary ----
112
- function onVisibilityChange() {
113
- if (document.visibilityState === 'hidden') {
114
- flushScrollDepth();
115
- if (activeSeconds > 0) {
116
- sendEngagement(creds, 'session_ping', 'lifecycle', { active_seconds: activeSeconds });
117
- }
118
- }
119
- }
120
- document.addEventListener('visibilitychange', onVisibilityChange, { signal });
121
- function onBeforeUnload() {
122
- flushScrollDepth();
123
- sendEngagement(creds, 'session_end', 'lifecycle', { active_seconds: activeSeconds });
124
- }
125
- window.addEventListener('beforeunload', onBeforeUnload, { signal });
126
- // ---- keepalive heartbeat (60s, lightweight, for realtime dashboard) ----
127
- function sendKeepalive() {
128
- if (document.visibilityState !== 'visible')
129
- return;
130
- sendEngagement(creds, 'heartbeat', 'lifecycle', { active_seconds: activeSeconds }, getSessionContextOnce());
131
- }
132
- // Send first heartbeat immediately to register the session context
133
- sendKeepalive();
134
- const keepaliveId = setInterval(sendKeepalive, KEEPALIVE_INTERVAL_MS);
135
- // ---- cleanup ----
136
- return () => {
137
- ac.abort();
138
- if (scrollTimer)
139
- clearTimeout(scrollTimer);
140
- clearInterval(activeTickId);
141
- clearInterval(keepaliveId);
142
- };
143
- }
@@ -1,3 +0,0 @@
1
- export { startEngagementTracking } from './engagement.js';
2
- export { getDefaultPathname, sendPageViewBeacon } from './pageview.js';
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/events/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA"}
@@ -1,2 +0,0 @@
1
- export { startEngagementTracking } from './engagement.js';
2
- export { getDefaultPathname, sendPageViewBeacon } from './pageview.js';
@@ -1,9 +0,0 @@
1
- import { type CollectCredentials } from '../collect.js';
2
- /** Current document path, or `/` outside a browser. */
3
- export declare function getDefaultPathname(): string;
4
- /**
5
- * Page view: sends slim `context` (per-event meta) plus `session_context` (session-stable meta).
6
- * The backend upserts the session row from `session_context` on every pageview.
7
- */
8
- export declare function sendPageViewBeacon(creds: CollectCredentials, path?: string): boolean;
9
- //# sourceMappingURL=pageview.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"pageview.d.ts","sourceRoot":"","sources":["../../src/events/pageview.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAE1E,uDAAuD;AACvD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,EAAE,IAAI,GAAE,MAA6B,GAAG,OAAO,CAa1G"}
@@ -1,24 +0,0 @@
1
- import { collectEventMeta, collectSessionMeta } from '../browser-meta.js';
2
- import { sendBeaconCollect } from '../collect.js';
3
- /** Current document path, or `/` outside a browser. */
4
- export function getDefaultPathname() {
5
- return typeof window !== 'undefined' ? window.location.pathname : '/';
6
- }
7
- /**
8
- * Page view: sends slim `context` (per-event meta) plus `session_context` (session-stable meta).
9
- * The backend upserts the session row from `session_context` on every pageview.
10
- */
11
- export function sendPageViewBeacon(creds, path = getDefaultPathname()) {
12
- return sendBeaconCollect({
13
- collectUrl: creds.collectUrl,
14
- credentials: { appId: creds.appId, apiToken: creds.apiToken },
15
- eventName: 'pageview',
16
- eventType: 'pageview',
17
- context: {
18
- ...collectEventMeta(),
19
- path,
20
- },
21
- data: {},
22
- sessionContext: collectSessionMeta(),
23
- });
24
- }
package/dist/nexly.d.ts DELETED
@@ -1,50 +0,0 @@
1
- /** Options for {@link Nexly}. `key` is the ingest API token (sent as `api_token` on the wire). */
2
- export type NexlyInit = {
3
- /** Override the ingest endpoint. Defaults to the Nexly production gateway. */
4
- collectUrl?: string;
5
- appId: string;
6
- key: string;
7
- };
8
- /** Allowed event types. The backend rejects any other value. */
9
- export type NexlyEventType = 'pageview' | 'engagement' | 'custom';
10
- export type NexlyEventInput = {
11
- name: string;
12
- type: NexlyEventType;
13
- data?: Record<string, unknown>;
14
- /** Merged on top of default browser context (see {@link collectBrowserMeta}) and current `path`. */
15
- context?: Record<string, unknown>;
16
- };
17
- /**
18
- * High-level browser client: page views, custom events, optional engagement auto-tracking.
19
- */
20
- export declare class Nexly {
21
- private static instance;
22
- /**
23
- * Creates (or replaces) the global singleton and returns it.
24
- * The provider calls this; plain functions use {@link getInstance}.
25
- */
26
- static init(options: NexlyInit): Nexly;
27
- /** Returns the singleton created by {@link init}, or `null` if not yet initialized. */
28
- static getInstance(): Nexly | null;
29
- readonly collectUrl: string;
30
- readonly appId: string;
31
- readonly key: string;
32
- constructor(init: NexlyInit);
33
- private get disabled();
34
- private get credentials();
35
- private defaultContext;
36
- /**
37
- * Sends a page view (`event_name` / `event_type`: `pageview`) with full browser context.
38
- */
39
- pageview(path?: string): boolean;
40
- /**
41
- * Sends a custom event. Default context is per-event meta + path; pass `context` to merge or override fields.
42
- * Includes session_context so the backend can upsert session metadata.
43
- */
44
- event(input: NexlyEventInput): boolean;
45
- /**
46
- * Starts scroll, click, focus, visibility, and heartbeat tracking. Returns a stop function.
47
- */
48
- startEngagement(): () => void;
49
- }
50
- //# sourceMappingURL=nexly.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"nexly.d.ts","sourceRoot":"","sources":["../src/nexly.ts"],"names":[],"mappings":"AAMA,kGAAkG;AAClG,MAAM,MAAM,SAAS,GAAG;IACtB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,gEAAgE;AAChE,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,YAAY,GAAG,QAAQ,CAAA;AAEjE,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,cAAc,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,oGAAoG;IACpG,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClC,CAAA;AAED;;GAEG;AACH,qBAAa,KAAK;IAChB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAqB;IAE5C;;;OAGG;IACH,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,KAAK;IAKtC,uFAAuF;IACvF,MAAM,CAAC,WAAW,IAAI,KAAK,GAAG,IAAI;IAIlC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;gBAER,IAAI,EAAE,SAAS;IAQ3B,OAAO,KAAK,QAAQ,GAEnB;IAED,OAAO,KAAK,WAAW,GAMtB;IAED,OAAO,CAAC,cAAc;IAOtB;;OAEG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO;IAKhC;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO;IAgBtC;;OAEG;IACH,eAAe,IAAI,MAAM,IAAI;CAI9B"}
package/dist/nexly.js DELETED
@@ -1,86 +0,0 @@
1
- import { collectEventMeta, collectSessionMeta } from './browser-meta.js';
2
- import { sendBeaconCollect } from './collect.js';
3
- import { getDefaultPathname, sendPageViewBeacon, startEngagementTracking } from './events/index.js';
4
- const DEFAULT_COLLECT_URL = 'https://gate.nexly.to/v1/collect';
5
- /**
6
- * High-level browser client: page views, custom events, optional engagement auto-tracking.
7
- */
8
- export class Nexly {
9
- static instance = null;
10
- /**
11
- * Creates (or replaces) the global singleton and returns it.
12
- * The provider calls this; plain functions use {@link getInstance}.
13
- */
14
- static init(options) {
15
- Nexly.instance = new Nexly(options);
16
- return Nexly.instance;
17
- }
18
- /** Returns the singleton created by {@link init}, or `null` if not yet initialized. */
19
- static getInstance() {
20
- return Nexly.instance;
21
- }
22
- collectUrl;
23
- appId;
24
- key;
25
- constructor(init) {
26
- this.collectUrl = (init.collectUrl ?? DEFAULT_COLLECT_URL).trim();
27
- this.appId = (init.appId ?? '').trim();
28
- this.key = (init.key ?? '').trim();
29
- if (!this.appId)
30
- console.warn('[Nexly] Missing appId — tracking will be disabled.');
31
- if (!this.key)
32
- console.warn('[Nexly] Missing key — tracking will be disabled.');
33
- }
34
- get disabled() {
35
- return !this.appId || !this.key;
36
- }
37
- get credentials() {
38
- return {
39
- collectUrl: this.collectUrl,
40
- appId: this.appId,
41
- apiToken: this.key,
42
- };
43
- }
44
- defaultContext() {
45
- return {
46
- ...collectEventMeta(),
47
- path: getDefaultPathname(),
48
- };
49
- }
50
- /**
51
- * Sends a page view (`event_name` / `event_type`: `pageview`) with full browser context.
52
- */
53
- pageview(path) {
54
- if (this.disabled)
55
- return false;
56
- return sendPageViewBeacon(this.credentials, path ?? getDefaultPathname());
57
- }
58
- /**
59
- * Sends a custom event. Default context is per-event meta + path; pass `context` to merge or override fields.
60
- * Includes session_context so the backend can upsert session metadata.
61
- */
62
- event(input) {
63
- if (this.disabled)
64
- return false;
65
- const context = input.context
66
- ? { ...this.defaultContext(), ...input.context }
67
- : this.defaultContext();
68
- return sendBeaconCollect({
69
- collectUrl: this.collectUrl,
70
- credentials: { appId: this.appId, apiToken: this.key },
71
- eventName: input.name,
72
- eventType: input.type,
73
- context,
74
- data: input.data ?? {},
75
- sessionContext: collectSessionMeta(),
76
- });
77
- }
78
- /**
79
- * Starts scroll, click, focus, visibility, and heartbeat tracking. Returns a stop function.
80
- */
81
- startEngagement() {
82
- if (this.disabled)
83
- return () => { };
84
- return startEngagementTracking(this.credentials);
85
- }
86
- }
package/dist/session.d.ts DELETED
@@ -1,11 +0,0 @@
1
- /**
2
- * Persistent anonymous visitor ID (localStorage). One per browser + origin.
3
- * Generated on first call, reused after.
4
- */
5
- export declare function getVisitorId(): string;
6
- /**
7
- * Session ID with 30-minute inactivity timeout (sessionStorage for tab scope, localStorage for timestamp).
8
- * New tab or > 30 min idle → new session.
9
- */
10
- export declare function getSessionId(): string;
11
- //# sourceMappingURL=session.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAoBA;;;GAGG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAUrC;AAED;;;GAGG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAmBrC"}
package/dist/session.js DELETED
@@ -1,52 +0,0 @@
1
- const VISITOR_KEY = 'trk_visitor_id';
2
- const SESSION_KEY = 'trk_session_id';
3
- const SESSION_TS_KEY = 'trk_session_ts';
4
- const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
5
- function generateId() {
6
- if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
7
- return crypto.randomUUID();
8
- }
9
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
10
- const r = (Math.random() * 16) | 0;
11
- return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
12
- });
13
- }
14
- function nowMs() {
15
- return Date.now();
16
- }
17
- /**
18
- * Persistent anonymous visitor ID (localStorage). One per browser + origin.
19
- * Generated on first call, reused after.
20
- */
21
- export function getVisitorId() {
22
- if (typeof localStorage === 'undefined') {
23
- return generateId();
24
- }
25
- let id = localStorage.getItem(VISITOR_KEY);
26
- if (!id) {
27
- id = generateId();
28
- localStorage.setItem(VISITOR_KEY, id);
29
- }
30
- return id;
31
- }
32
- /**
33
- * Session ID with 30-minute inactivity timeout (sessionStorage for tab scope, localStorage for timestamp).
34
- * New tab or > 30 min idle → new session.
35
- */
36
- export function getSessionId() {
37
- if (typeof sessionStorage === 'undefined') {
38
- return generateId();
39
- }
40
- const existing = sessionStorage.getItem(SESSION_KEY);
41
- const tsRaw = localStorage.getItem(SESSION_TS_KEY);
42
- const lastTs = tsRaw ? Number(tsRaw) : 0;
43
- const now = nowMs();
44
- if (existing && lastTs && now - lastTs < SESSION_TIMEOUT_MS) {
45
- localStorage.setItem(SESSION_TS_KEY, String(now));
46
- return existing;
47
- }
48
- const id = generateId();
49
- sessionStorage.setItem(SESSION_KEY, id);
50
- localStorage.setItem(SESSION_TS_KEY, String(now));
51
- return id;
52
- }