@nexly/core 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nexly
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # @nexly/core
2
+
3
+ Framework-agnostic **Nexly** browser ingest SDK: `Nexly` client, `sendBeacon` helpers, browser context, optional engagement listeners.
4
+
5
+ For **React**, use **`@nexly/react-web`** (`NexlyProvider` with optional `autoPageView` / `autoEngagement`, `useNexlyClient`).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @nexly/core
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```ts
16
+ import { Nexly } from '@nexly/core'
17
+
18
+ const nexly = new Nexly(options)
19
+
20
+ nexly.pageview()
21
+ nexly.event({ name: 'click', type: 'engagement', data: { id: 'x' } })
22
+ ```
23
+
24
+ `options` matches the `NexlyInit` type (your host app builds this object).
25
+
26
+ ## API
27
+
28
+ Published **`.d.ts`** declarations list exports and types (`Nexly`, `sendBeaconCollect`, `collectBrowserMeta`, `startEngagementTracking`, and related).
29
+
30
+ ## License
31
+
32
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Collects browser and environment metadata for the ingest `context` object (snake_case keys).
3
+ * Includes `visitor_id` and `session_id` from session management.
4
+ * Safe to call only in a browser; returns `{}` on the server or if APIs throw.
5
+ *
6
+ * Does not include geo (country/region/city) — that requires server-side IP lookup.
7
+ */
8
+ export declare function collectBrowserMeta(): Record<string, unknown>;
9
+ //# sourceMappingURL=browser-meta.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-meta.d.ts","sourceRoot":"","sources":["../src/browser-meta.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAYH,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmG5D"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Collects browser and environment metadata for the ingest `context` object (snake_case keys).
3
+ * Includes `visitor_id` and `session_id` from session management.
4
+ * Safe to call only in a browser; returns `{}` on the server or if APIs throw.
5
+ *
6
+ * Does not include geo (country/region/city) — that requires server-side IP lookup.
7
+ */
8
+ import { getSessionId, getVisitorId } from './session.js';
9
+ function safe(fn) {
10
+ try {
11
+ return fn();
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
17
+ export function collectBrowserMeta() {
18
+ if (typeof window === 'undefined' || typeof navigator === 'undefined') {
19
+ return {};
20
+ }
21
+ const out = {};
22
+ out.visitor_id = safe(() => getVisitorId());
23
+ out.session_id = safe(() => getSessionId());
24
+ out.url = safe(() => window.location.href);
25
+ out.pathname = safe(() => window.location.pathname);
26
+ const ref = safe(() => document.referrer);
27
+ if (ref) {
28
+ out.referrer = ref;
29
+ }
30
+ out.language = safe(() => navigator.language);
31
+ const langs = safe(() => navigator.languages);
32
+ if (langs?.length) {
33
+ out.languages = [...langs];
34
+ }
35
+ out.timezone = safe(() => Intl.DateTimeFormat().resolvedOptions().timeZone);
36
+ out.screen_width = safe(() => window.screen.width);
37
+ out.screen_height = safe(() => window.screen.height);
38
+ out.avail_screen_width = safe(() => window.screen.availWidth);
39
+ out.avail_screen_height = safe(() => window.screen.availHeight);
40
+ out.color_depth = safe(() => window.screen.colorDepth);
41
+ out.viewport_width = safe(() => window.innerWidth);
42
+ out.viewport_height = safe(() => window.innerHeight);
43
+ out.device_pixel_ratio = safe(() => window.devicePixelRatio);
44
+ out.user_agent = safe(() => navigator.userAgent);
45
+ out.navigator_platform = safe(() => navigator.platform);
46
+ out.cookie_enabled = safe(() => navigator.cookieEnabled);
47
+ out.online = safe(() => navigator.onLine);
48
+ out.hardware_concurrency = safe(() => navigator.hardwareConcurrency);
49
+ out.max_touch_points = safe(() => navigator.maxTouchPoints);
50
+ const conn = safe(() => navigator.connection);
51
+ if (conn) {
52
+ const downlink = safe(() => conn.downlink);
53
+ const effectiveType = safe(() => conn.effectiveType);
54
+ const rtt = safe(() => conn.rtt);
55
+ if (downlink !== undefined) {
56
+ out.network_downlink_mbps = downlink;
57
+ }
58
+ if (effectiveType !== undefined) {
59
+ out.network_effective_type = effectiveType;
60
+ }
61
+ if (rtt !== undefined) {
62
+ out.network_rtt_ms = rtt;
63
+ }
64
+ }
65
+ const ud = safe(() => navigator.userAgentData);
66
+ if (ud) {
67
+ const mobile = safe(() => ud.mobile);
68
+ const platform = safe(() => ud.platform);
69
+ if (mobile !== undefined) {
70
+ out.ua_mobile = mobile;
71
+ }
72
+ if (platform) {
73
+ out.ua_platform = platform;
74
+ }
75
+ const brands = safe(() => ud.brands);
76
+ if (brands?.length) {
77
+ out.ua_brands = brands.map((b) => `${b.brand} ${b.version}`).join(', ');
78
+ }
79
+ }
80
+ if (typeof window.matchMedia === 'function') {
81
+ const dark = safe(() => window.matchMedia('(prefers-color-scheme: dark)').matches);
82
+ if (dark !== undefined) {
83
+ out.prefers_color_scheme = dark ? 'dark' : 'light';
84
+ }
85
+ const reduced = safe(() => window.matchMedia('(prefers-reduced-motion: reduce)').matches);
86
+ if (reduced !== undefined) {
87
+ out.prefers_reduced_motion = reduced;
88
+ }
89
+ }
90
+ if (typeof document !== 'undefined' && document.documentElement) {
91
+ const docW = safe(() => document.documentElement.clientWidth);
92
+ const docH = safe(() => document.documentElement.clientHeight);
93
+ if (docW !== undefined) {
94
+ out.document_client_width = docW;
95
+ }
96
+ if (docH !== undefined) {
97
+ out.document_client_height = docH;
98
+ }
99
+ }
100
+ return Object.fromEntries(Object.entries(out).filter(([, v]) => v !== undefined && v !== '' && !(Array.isArray(v) && v.length === 0)));
101
+ }
@@ -0,0 +1,35 @@
1
+ /** Auth fields for ingest (no transport URL). */
2
+ export type IngestCredentials = {
3
+ appId: string;
4
+ apiToken: string;
5
+ };
6
+ /**
7
+ * Event envelope:
8
+ * - `context` — built-in traffic / device / locale (referrer, url, utm_*, os, device, …). Not owner-registered.
9
+ * - `data` — owner-defined custom properties (later: only keys registered for the app).
10
+ */
11
+ export type TrackEventInput = {
12
+ credentials: IngestCredentials;
13
+ eventName: string;
14
+ eventType: string;
15
+ context?: Record<string, unknown>;
16
+ data?: Record<string, unknown>;
17
+ };
18
+ /** Full send: transport URL + event envelope. */
19
+ export type SendBeaconCollectInput = TrackEventInput & {
20
+ collectUrl: string;
21
+ };
22
+ /** Flat credentials + URL for small apps and event helpers (maps to {@link TrackEventInput.credentials}). */
23
+ export type CollectCredentials = {
24
+ collectUrl: string;
25
+ } & IngestCredentials;
26
+ /**
27
+ * Builds the JSON body for POST /v1/collect (snake_case wire format).
28
+ * Duplicates `context.path` to top-level `path` when present.
29
+ */
30
+ export declare function buildCollectPayload(input: TrackEventInput): Record<string, unknown>;
31
+ /**
32
+ * Packages the payload and sends it via `navigator.sendBeacon` (survives page unload).
33
+ */
34
+ export declare function sendBeaconCollect(input: SendBeaconCollectInput): boolean;
35
+ //# sourceMappingURL=collect.d.ts.map
@@ -0,0 +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;;;;GAIG;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;CAC/B,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;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAiBnF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CASxE"}
@@ -0,0 +1,38 @@
1
+ function isPlainObject(v) {
2
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
3
+ }
4
+ /**
5
+ * Builds the JSON body for POST /v1/collect (snake_case wire format).
6
+ * Duplicates `context.path` to top-level `path` when present.
7
+ */
8
+ export function buildCollectPayload(input) {
9
+ const context = isPlainObject(input.context) ? input.context : {};
10
+ const data = isPlainObject(input.data) ? input.data : {};
11
+ const pathFromContext = typeof context.path === 'string' ? context.path : '';
12
+ const path = pathFromContext;
13
+ const body = {
14
+ app_id: input.credentials.appId,
15
+ api_token: input.credentials.apiToken,
16
+ event_name: input.eventName,
17
+ event_type: input.eventType,
18
+ context,
19
+ data,
20
+ };
21
+ if (path) {
22
+ body.path = path;
23
+ }
24
+ return body;
25
+ }
26
+ /**
27
+ * Packages the payload and sends it via `navigator.sendBeacon` (survives page unload).
28
+ */
29
+ export function sendBeaconCollect(input) {
30
+ const url = input.collectUrl.trim();
31
+ if (!url) {
32
+ return false;
33
+ }
34
+ const payload = buildCollectPayload(input);
35
+ const body = JSON.stringify(payload);
36
+ const blob = new Blob([body], { type: 'application/json' });
37
+ return navigator.sendBeacon(url, blob);
38
+ }
@@ -0,0 +1,9 @@
1
+ import { type CollectCredentials } from '../collect.js';
2
+ type StopFn = () => void;
3
+ /**
4
+ * Attaches engagement listeners (scroll, click, input focus, visibility, heartbeat).
5
+ * Returns a cleanup function that removes all listeners and timers.
6
+ */
7
+ export declare function startEngagementTracking(creds: CollectCredentials): StopFn;
8
+ export {};
9
+ //# sourceMappingURL=engagement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engagement.d.ts","sourceRoot":"","sources":["../../src/events/engagement.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAqB1E,KAAK,MAAM,GAAG,MAAM,IAAI,CAAA;AAExB;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,CA2HzE"}
@@ -0,0 +1,140 @@
1
+ import { collectBrowserMeta } from '../browser-meta.js';
2
+ import { sendBeaconCollect } from '../collect.js';
3
+ const CLICK_THROTTLE_MS = 500;
4
+ const HEARTBEAT_INTERVAL_MS = 30_000;
5
+ const SCROLL_DEBOUNCE_MS = 300;
6
+ function makeContext() {
7
+ return collectBrowserMeta();
8
+ }
9
+ function sendEngagement(creds, eventName, eventType, data) {
10
+ sendBeaconCollect({
11
+ collectUrl: creds.collectUrl,
12
+ credentials: { appId: creds.appId, apiToken: creds.apiToken },
13
+ eventName,
14
+ eventType,
15
+ context: makeContext(),
16
+ data,
17
+ });
18
+ }
19
+ /**
20
+ * Attaches engagement listeners (scroll, click, input focus, visibility, heartbeat).
21
+ * Returns a cleanup function that removes all listeners and timers.
22
+ */
23
+ export function startEngagementTracking(creds) {
24
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
25
+ return () => { };
26
+ }
27
+ const ac = new AbortController();
28
+ const { signal } = ac;
29
+ // ---- scroll depth ----
30
+ let maxScrollPercent = 0;
31
+ let scrollTimer = null;
32
+ function updateScrollPercent() {
33
+ const docH = document.documentElement.scrollHeight;
34
+ const viewH = window.innerHeight;
35
+ if (docH <= viewH) {
36
+ maxScrollPercent = 100;
37
+ return;
38
+ }
39
+ const pct = Math.round((window.scrollY / (docH - viewH)) * 100);
40
+ if (pct > maxScrollPercent) {
41
+ maxScrollPercent = pct;
42
+ }
43
+ }
44
+ function onScroll() {
45
+ if (scrollTimer)
46
+ clearTimeout(scrollTimer);
47
+ scrollTimer = setTimeout(updateScrollPercent, SCROLL_DEBOUNCE_MS);
48
+ }
49
+ window.addEventListener('scroll', onScroll, { passive: true, signal });
50
+ function flushScrollDepth() {
51
+ updateScrollPercent();
52
+ if (maxScrollPercent > 0) {
53
+ sendEngagement(creds, 'scroll_depth', 'engagement', { max_percent: maxScrollPercent });
54
+ }
55
+ }
56
+ // ---- click ----
57
+ let lastClickTs = 0;
58
+ function onClick(e) {
59
+ const now = Date.now();
60
+ if (now - lastClickTs < CLICK_THROTTLE_MS)
61
+ return;
62
+ lastClickTs = now;
63
+ const el = e.target;
64
+ if (!el)
65
+ return;
66
+ const tag = el.tagName?.toLowerCase() ?? '';
67
+ const rawText = el.innerText ?? '';
68
+ const text = rawText.slice(0, 80).trim();
69
+ sendEngagement(creds, 'click', 'engagement', {
70
+ tag,
71
+ ...(text ? { text } : {}),
72
+ });
73
+ }
74
+ document.addEventListener('click', onClick, { signal });
75
+ // ---- input focus (once per element per session) ----
76
+ const focusedElements = new WeakSet();
77
+ function onFocusIn(e) {
78
+ const el = e.target;
79
+ if (!el)
80
+ return;
81
+ const tag = el.tagName?.toLowerCase() ?? '';
82
+ if (tag !== 'input' && tag !== 'textarea' && tag !== 'select')
83
+ return;
84
+ if (focusedElements.has(el))
85
+ return;
86
+ focusedElements.add(el);
87
+ sendEngagement(creds, 'input_focus', 'engagement', {
88
+ tag,
89
+ ...(el.name ? { name: el.name } : {}),
90
+ ...(el.type ? { type: el.type } : {}),
91
+ });
92
+ }
93
+ document.addEventListener('focusin', onFocusIn, { signal });
94
+ // ---- visibility / visit duration ----
95
+ let visibleSince = document.visibilityState === 'visible' ? Date.now() : 0;
96
+ let accumulatedVisibleMs = 0;
97
+ function onVisibilityChange() {
98
+ if (document.visibilityState === 'hidden') {
99
+ if (visibleSince > 0) {
100
+ accumulatedVisibleMs += Date.now() - visibleSince;
101
+ visibleSince = 0;
102
+ }
103
+ flushScrollDepth();
104
+ const secs = Math.round(accumulatedVisibleMs / 1000);
105
+ if (secs > 0) {
106
+ sendEngagement(creds, 'visibility_change', 'lifecycle', { visible_seconds: secs, state: 'hidden' });
107
+ }
108
+ }
109
+ else {
110
+ visibleSince = Date.now();
111
+ }
112
+ }
113
+ document.addEventListener('visibilitychange', onVisibilityChange, { signal });
114
+ function onBeforeUnload() {
115
+ if (visibleSince > 0) {
116
+ accumulatedVisibleMs += Date.now() - visibleSince;
117
+ visibleSince = 0;
118
+ }
119
+ flushScrollDepth();
120
+ const secs = Math.round(accumulatedVisibleMs / 1000);
121
+ sendEngagement(creds, 'visibility_change', 'lifecycle', { visible_seconds: secs, state: 'unload' });
122
+ }
123
+ window.addEventListener('beforeunload', onBeforeUnload, { signal });
124
+ // ---- heartbeat ----
125
+ const startTime = Date.now();
126
+ function sendHeartbeat() {
127
+ if (document.visibilityState !== 'visible')
128
+ return;
129
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
130
+ sendEngagement(creds, 'heartbeat', 'lifecycle', { elapsed_seconds: elapsed });
131
+ }
132
+ const heartbeatId = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS);
133
+ // ---- cleanup ----
134
+ return () => {
135
+ ac.abort();
136
+ if (scrollTimer)
137
+ clearTimeout(scrollTimer);
138
+ clearInterval(heartbeatId);
139
+ };
140
+ }
@@ -0,0 +1,3 @@
1
+ export { startEngagementTracking } from './engagement.js';
2
+ export { getDefaultPathname, sendPageViewBeacon } from './pageview.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,2 @@
1
+ export { startEngagementTracking } from './engagement.js';
2
+ export { getDefaultPathname, sendPageViewBeacon } from './pageview.js';
@@ -0,0 +1,8 @@
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: fills `context` with {@link collectBrowserMeta} and sets `path` (overrides pathname from the location snapshot).
6
+ */
7
+ export declare function sendPageViewBeacon(creds: CollectCredentials, path?: string): boolean;
8
+ //# sourceMappingURL=pageview.d.ts.map
@@ -0,0 +1 @@
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;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,EAAE,IAAI,GAAE,MAA6B,GAAG,OAAO,CAY1G"}
@@ -0,0 +1,22 @@
1
+ import { collectBrowserMeta } 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: fills `context` with {@link collectBrowserMeta} and sets `path` (overrides pathname from the location snapshot).
9
+ */
10
+ export function sendPageViewBeacon(creds, path = getDefaultPathname()) {
11
+ return sendBeaconCollect({
12
+ collectUrl: creds.collectUrl,
13
+ credentials: { appId: creds.appId, apiToken: creds.apiToken },
14
+ eventName: 'pageview',
15
+ eventType: 'pageview',
16
+ context: {
17
+ ...collectBrowserMeta(),
18
+ path,
19
+ },
20
+ data: {},
21
+ });
22
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Browser event ingest client (public API).
3
+ */
4
+ export { Nexly, type NexlyEventInput, type NexlyInit } from './nexly.js';
5
+ export { collectBrowserMeta } 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';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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,SAAS,EAAE,MAAM,YAAY,CAAA;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AACtD,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"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Browser event ingest client (public API).
3
+ */
4
+ export { Nexly } from './nexly.js';
5
+ export { collectBrowserMeta } 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';
@@ -0,0 +1,37 @@
1
+ /** Options for {@link Nexly}. `key` is the ingest API token (sent as `api_token` on the wire). */
2
+ export type NexlyInit = {
3
+ collectUrl: string;
4
+ appId: string;
5
+ key: string;
6
+ };
7
+ export type NexlyEventInput = {
8
+ name: string;
9
+ type: string;
10
+ data?: Record<string, unknown>;
11
+ /** Merged on top of default browser context (see {@link collectBrowserMeta}) and current `path`. */
12
+ context?: Record<string, unknown>;
13
+ };
14
+ /**
15
+ * High-level browser client: page views, custom events, optional engagement auto-tracking.
16
+ */
17
+ export declare class Nexly {
18
+ readonly collectUrl: string;
19
+ readonly appId: string;
20
+ readonly key: string;
21
+ constructor(init: NexlyInit);
22
+ private get credentials();
23
+ private defaultContext;
24
+ /**
25
+ * Sends a page view (`event_name` / `event_type`: `pageview`) with full browser context.
26
+ */
27
+ pageview(path?: string): boolean;
28
+ /**
29
+ * Sends a custom event. Default context is browser meta + path; pass `context` to merge or override fields.
30
+ */
31
+ event(input: NexlyEventInput): boolean;
32
+ /**
33
+ * Starts scroll, click, focus, visibility, and heartbeat tracking. Returns a stop function.
34
+ */
35
+ startEngagement(): () => void;
36
+ }
37
+ //# sourceMappingURL=nexly.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nexly.d.ts","sourceRoot":"","sources":["../src/nexly.ts"],"names":[],"mappings":"AAIA,kGAAkG;AAClG,MAAM,MAAM,SAAS,GAAG;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,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,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;gBAER,IAAI,EAAE,SAAS;IAM3B,OAAO,KAAK,WAAW,GAMtB;IAED,OAAO,CAAC,cAAc;IAOtB;;OAEG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO;IAIhC;;OAEG;IACH,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO;IActC;;OAEG;IACH,eAAe,IAAI,MAAM,IAAI;CAG9B"}
package/dist/nexly.js ADDED
@@ -0,0 +1,57 @@
1
+ import { collectBrowserMeta } from './browser-meta.js';
2
+ import { sendBeaconCollect } from './collect.js';
3
+ import { getDefaultPathname, sendPageViewBeacon, startEngagementTracking } from './events/index.js';
4
+ /**
5
+ * High-level browser client: page views, custom events, optional engagement auto-tracking.
6
+ */
7
+ export class Nexly {
8
+ collectUrl;
9
+ appId;
10
+ key;
11
+ constructor(init) {
12
+ this.collectUrl = init.collectUrl.trim();
13
+ this.appId = init.appId.trim();
14
+ this.key = init.key.trim();
15
+ }
16
+ get credentials() {
17
+ return {
18
+ collectUrl: this.collectUrl,
19
+ appId: this.appId,
20
+ apiToken: this.key,
21
+ };
22
+ }
23
+ defaultContext() {
24
+ return {
25
+ ...collectBrowserMeta(),
26
+ path: getDefaultPathname(),
27
+ };
28
+ }
29
+ /**
30
+ * Sends a page view (`event_name` / `event_type`: `pageview`) with full browser context.
31
+ */
32
+ pageview(path) {
33
+ return sendPageViewBeacon(this.credentials, path ?? getDefaultPathname());
34
+ }
35
+ /**
36
+ * Sends a custom event. Default context is browser meta + path; pass `context` to merge or override fields.
37
+ */
38
+ event(input) {
39
+ const context = input.context
40
+ ? { ...this.defaultContext(), ...input.context }
41
+ : this.defaultContext();
42
+ return sendBeaconCollect({
43
+ collectUrl: this.collectUrl,
44
+ credentials: { appId: this.appId, apiToken: this.key },
45
+ eventName: input.name,
46
+ eventType: input.type,
47
+ context,
48
+ data: input.data ?? {},
49
+ });
50
+ }
51
+ /**
52
+ * Starts scroll, click, focus, visibility, and heartbeat tracking. Returns a stop function.
53
+ */
54
+ startEngagement() {
55
+ return startEngagementTracking(this.credentials);
56
+ }
57
+ }
@@ -0,0 +1,11 @@
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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,52 @@
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
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@nexly/core",
3
+ "version": "0.3.0",
4
+ "description": "Nexly browser ingest core (sendBeacon, Nexly client, context, engagement)",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "nexly",
8
+ "analytics",
9
+ "tracking",
10
+ "sendBeacon",
11
+ "browser",
12
+ "ingest"
13
+ ],
14
+ "type": "module",
15
+ "sideEffects": false,
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "main": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js",
30
+ "default": "./dist/index.js"
31
+ }
32
+ },
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.build.json",
35
+ "clean": "rm -rf dist"
36
+ },
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "devDependencies": {
41
+ "typescript": "~6.0.2"
42
+ }
43
+ }