@nexly/web 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/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/beacon.d.ts +13 -0
- package/dist/beacon.d.ts.map +1 -0
- package/dist/beacon.js +18 -0
- package/dist/browser-meta.d.ts +12 -0
- package/dist/browser-meta.d.ts.map +1 -0
- package/dist/browser-meta.js +98 -0
- package/dist/events/engagement.d.ts +9 -0
- package/dist/events/engagement.d.ts.map +1 -0
- package/dist/events/engagement.js +129 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +2 -0
- package/dist/events/pageview.d.ts +8 -0
- package/dist/events/pageview.d.ts.map +1 -0
- package/dist/events/pageview.js +20 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/nexly.d.ts +18 -0
- package/dist/nexly.d.ts.map +1 -0
- package/dist/nexly.js +47 -0
- package/dist/session.d.ts +9 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +50 -0
- package/package.json +46 -0
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,35 @@
|
|
|
1
|
+
# @nexly/web
|
|
2
|
+
|
|
3
|
+
Browser ingest SDK for Nexly: `sendBeacon` transport, DOM metadata, engagement listeners. Implements the abstract [`NexlyBase`](https://www.npmjs.com/package/@nexly/core) from `@nexly/core` for the browser runtime.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @nexly/web
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Nexly } from '@nexly/web'
|
|
15
|
+
|
|
16
|
+
const client = Nexly.init({
|
|
17
|
+
appId: 'your-app-id',
|
|
18
|
+
key: 'your-ingest-key',
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
client.pageview()
|
|
22
|
+
client.event({ name: 'cta_clicked', type: 'custom', data: { placement: 'hero' } })
|
|
23
|
+
|
|
24
|
+
const stopEngagement = client.startEngagement()
|
|
25
|
+
// stopEngagement() on SPA unmount / route change if needed
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## What you get
|
|
29
|
+
|
|
30
|
+
- `Nexly` — browser client (auto session + visitor IDs via `localStorage`/`sessionStorage`).
|
|
31
|
+
- `collectSessionMeta()`, `collectEventMeta()`, `collectBrowserMeta()` — reusable metadata collectors.
|
|
32
|
+
- `startEngagementTracking(creds)` — standalone engagement attach (scroll / click / focus / visibility / keepalive).
|
|
33
|
+
- `sendBeaconCollect(input)` — raw `sendBeacon` helper.
|
|
34
|
+
|
|
35
|
+
For React bindings use [`@nexly/react-web`](https://www.npmjs.com/package/@nexly/react-web); for Next.js App Router use [`@nexly/next`](https://www.npmjs.com/package/@nexly/next); for React Native use [`@nexly/react-native`](https://www.npmjs.com/package/@nexly/react-native).
|
package/dist/beacon.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type CollectCredentials, type TrackEventInput } from '@nexly/core';
|
|
2
|
+
export type SendBeaconCollectInput = TrackEventInput & {
|
|
3
|
+
collectUrl: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Serializes the payload and dispatches it via `navigator.sendBeacon`
|
|
7
|
+
* (survives page unload). Returns the beacon queue acceptance flag.
|
|
8
|
+
*/
|
|
9
|
+
export declare function sendBeaconCollect(input: SendBeaconCollectInput): boolean;
|
|
10
|
+
/** Internal helper: send a pre-built payload via sendBeacon. */
|
|
11
|
+
export declare function sendPayloadBeacon(url: string, payload: Record<string, unknown>): boolean;
|
|
12
|
+
export type { CollectCredentials };
|
|
13
|
+
//# sourceMappingURL=beacon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"beacon.d.ts","sourceRoot":"","sources":["../src/beacon.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAA;AAEhG,MAAM,MAAM,sBAAsB,GAAG,eAAe,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAAA;AAE7E;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAKxE;AAED,gEAAgE;AAChE,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAIxF;AAED,YAAY,EAAE,kBAAkB,EAAE,CAAA"}
|
package/dist/beacon.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { buildCollectPayload } from '@nexly/core';
|
|
2
|
+
/**
|
|
3
|
+
* Serializes the payload and dispatches it via `navigator.sendBeacon`
|
|
4
|
+
* (survives page unload). Returns the beacon queue acceptance flag.
|
|
5
|
+
*/
|
|
6
|
+
export function sendBeaconCollect(input) {
|
|
7
|
+
const url = (input.collectUrl ?? '').trim();
|
|
8
|
+
if (!url)
|
|
9
|
+
return false;
|
|
10
|
+
const payload = buildCollectPayload(input);
|
|
11
|
+
return sendPayloadBeacon(url, payload);
|
|
12
|
+
}
|
|
13
|
+
/** Internal helper: send a pre-built payload via sendBeacon. */
|
|
14
|
+
export function sendPayloadBeacon(url, payload) {
|
|
15
|
+
const body = JSON.stringify(payload);
|
|
16
|
+
const blob = new Blob([body], { type: 'text/plain' });
|
|
17
|
+
return navigator.sendBeacon(url, blob);
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser metadata collection, split into session-stable and per-event fields.
|
|
3
|
+
*
|
|
4
|
+
* Does not include geo (country/region/city) — that requires server-side IP lookup.
|
|
5
|
+
*/
|
|
6
|
+
/** Session-stable metadata: user agent, screen, language, timezone, etc. */
|
|
7
|
+
export declare function collectSessionMeta(): Record<string, unknown>;
|
|
8
|
+
/** Per-event volatile metadata: visitor/session IDs, current URL/pathname. */
|
|
9
|
+
export declare function collectEventMeta(): Record<string, unknown>;
|
|
10
|
+
/** Legacy: full browser metadata (session + event merged). */
|
|
11
|
+
export declare function collectBrowserMeta(): Record<string, unknown>;
|
|
12
|
+
//# 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;;;;GAIG;AAkBH,4EAA4E;AAC5E,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmE5D;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAW1D;AAED,8DAA8D;AAC9D,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE5D"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser metadata collection, split into session-stable and per-event fields.
|
|
3
|
+
*
|
|
4
|
+
* Does not include geo (country/region/city) — that requires server-side IP lookup.
|
|
5
|
+
*/
|
|
6
|
+
import { getSessionId, getVisitorId } from './session.js';
|
|
7
|
+
function safe(fn) {
|
|
8
|
+
try {
|
|
9
|
+
return fn();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function filterEmpty(obj) {
|
|
16
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined && v !== '' && !(Array.isArray(v) && v.length === 0)));
|
|
17
|
+
}
|
|
18
|
+
/** Session-stable metadata: user agent, screen, language, timezone, etc. */
|
|
19
|
+
export function collectSessionMeta() {
|
|
20
|
+
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
const out = {};
|
|
24
|
+
out.language = safe(() => navigator.language);
|
|
25
|
+
const langs = safe(() => navigator.languages);
|
|
26
|
+
if (langs?.length) {
|
|
27
|
+
out.languages = [...langs];
|
|
28
|
+
}
|
|
29
|
+
out.timezone = safe(() => Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
30
|
+
out.screen_width = safe(() => window.screen.width);
|
|
31
|
+
out.screen_height = safe(() => window.screen.height);
|
|
32
|
+
out.avail_screen_width = safe(() => window.screen.availWidth);
|
|
33
|
+
out.avail_screen_height = safe(() => window.screen.availHeight);
|
|
34
|
+
out.color_depth = safe(() => window.screen.colorDepth);
|
|
35
|
+
out.viewport_width = safe(() => window.innerWidth);
|
|
36
|
+
out.viewport_height = safe(() => window.innerHeight);
|
|
37
|
+
out.device_pixel_ratio = safe(() => window.devicePixelRatio);
|
|
38
|
+
const ref = safe(() => document.referrer);
|
|
39
|
+
if (ref) {
|
|
40
|
+
out.referrer = ref;
|
|
41
|
+
}
|
|
42
|
+
out.online = safe(() => navigator.onLine);
|
|
43
|
+
out.user_agent = safe(() => navigator.userAgent);
|
|
44
|
+
out.navigator_platform = safe(() => navigator.platform);
|
|
45
|
+
out.cookie_enabled = safe(() => navigator.cookieEnabled);
|
|
46
|
+
out.hardware_concurrency = safe(() => navigator.hardwareConcurrency);
|
|
47
|
+
out.max_touch_points = safe(() => navigator.maxTouchPoints);
|
|
48
|
+
const conn = safe(() => navigator.connection);
|
|
49
|
+
if (conn) {
|
|
50
|
+
const downlink = safe(() => conn.downlink);
|
|
51
|
+
const effectiveType = safe(() => conn.effectiveType);
|
|
52
|
+
const rtt = safe(() => conn.rtt);
|
|
53
|
+
if (downlink !== undefined)
|
|
54
|
+
out.network_downlink_mbps = downlink;
|
|
55
|
+
if (effectiveType !== undefined)
|
|
56
|
+
out.network_effective_type = effectiveType;
|
|
57
|
+
if (rtt !== undefined)
|
|
58
|
+
out.network_rtt_ms = rtt;
|
|
59
|
+
}
|
|
60
|
+
const ud = safe(() => navigator.userAgentData);
|
|
61
|
+
if (ud) {
|
|
62
|
+
const mobile = safe(() => ud.mobile);
|
|
63
|
+
const platform = safe(() => ud.platform);
|
|
64
|
+
if (mobile !== undefined)
|
|
65
|
+
out.ua_mobile = mobile;
|
|
66
|
+
if (platform)
|
|
67
|
+
out.ua_platform = platform;
|
|
68
|
+
const brands = safe(() => ud.brands);
|
|
69
|
+
if (brands?.length) {
|
|
70
|
+
out.ua_brands = brands.map((b) => `${b.brand} ${b.version}`).join(', ');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (typeof window.matchMedia === 'function') {
|
|
74
|
+
const dark = safe(() => window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
75
|
+
if (dark !== undefined)
|
|
76
|
+
out.prefers_color_scheme = dark ? 'dark' : 'light';
|
|
77
|
+
const reduced = safe(() => window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
|
78
|
+
if (reduced !== undefined)
|
|
79
|
+
out.prefers_reduced_motion = reduced;
|
|
80
|
+
}
|
|
81
|
+
return filterEmpty(out);
|
|
82
|
+
}
|
|
83
|
+
/** Per-event volatile metadata: visitor/session IDs, current URL/pathname. */
|
|
84
|
+
export function collectEventMeta() {
|
|
85
|
+
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
const out = {};
|
|
89
|
+
out.visitor_id = safe(() => getVisitorId());
|
|
90
|
+
out.session_id = safe(() => getSessionId());
|
|
91
|
+
out.url = safe(() => window.location.href);
|
|
92
|
+
out.pathname = safe(() => window.location.pathname);
|
|
93
|
+
return filterEmpty(out);
|
|
94
|
+
}
|
|
95
|
+
/** Legacy: full browser metadata (session + event merged). */
|
|
96
|
+
export function collectBrowserMeta() {
|
|
97
|
+
return { ...collectSessionMeta(), ...collectEventMeta() };
|
|
98
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CollectCredentials } from '@nexly/core';
|
|
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
|
+
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":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AA2BrD,KAAK,MAAM,GAAG,MAAM,IAAI,CAAA;AAExB;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,CA8GzE"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { sendBeaconCollect } from '../beacon.js';
|
|
2
|
+
import { collectEventMeta, collectSessionMeta } from '../browser-meta.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
|
+
export function startEngagementTracking(creds) {
|
|
23
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
24
|
+
return () => { };
|
|
25
|
+
}
|
|
26
|
+
const ac = new AbortController();
|
|
27
|
+
const { signal } = ac;
|
|
28
|
+
let sessionContextSent = false;
|
|
29
|
+
function getSessionContextOnce() {
|
|
30
|
+
if (sessionContextSent)
|
|
31
|
+
return undefined;
|
|
32
|
+
sessionContextSent = true;
|
|
33
|
+
return collectSessionMeta();
|
|
34
|
+
}
|
|
35
|
+
let activeSeconds = 0;
|
|
36
|
+
const activeTickId = setInterval(() => {
|
|
37
|
+
if (document.visibilityState === 'visible')
|
|
38
|
+
activeSeconds++;
|
|
39
|
+
}, ACTIVE_TICK_MS);
|
|
40
|
+
let maxScrollPercent = 0;
|
|
41
|
+
let scrollTimer = null;
|
|
42
|
+
function updateScrollPercent() {
|
|
43
|
+
const docH = document.documentElement.scrollHeight;
|
|
44
|
+
const viewH = window.innerHeight;
|
|
45
|
+
if (docH <= viewH) {
|
|
46
|
+
maxScrollPercent = 100;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const pct = Math.round((window.scrollY / (docH - viewH)) * 100);
|
|
50
|
+
if (pct > maxScrollPercent)
|
|
51
|
+
maxScrollPercent = pct;
|
|
52
|
+
}
|
|
53
|
+
function onScroll() {
|
|
54
|
+
if (scrollTimer)
|
|
55
|
+
clearTimeout(scrollTimer);
|
|
56
|
+
scrollTimer = setTimeout(updateScrollPercent, SCROLL_DEBOUNCE_MS);
|
|
57
|
+
}
|
|
58
|
+
window.addEventListener('scroll', onScroll, { passive: true, signal });
|
|
59
|
+
function flushScrollDepth() {
|
|
60
|
+
updateScrollPercent();
|
|
61
|
+
if (maxScrollPercent > 0) {
|
|
62
|
+
sendEngagement(creds, 'scroll_depth', 'engagement', { max_percent: maxScrollPercent });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let lastClickTs = 0;
|
|
66
|
+
function onClick(e) {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
if (now - lastClickTs < CLICK_THROTTLE_MS)
|
|
69
|
+
return;
|
|
70
|
+
lastClickTs = now;
|
|
71
|
+
const el = e.target;
|
|
72
|
+
if (!el)
|
|
73
|
+
return;
|
|
74
|
+
const tag = el.tagName?.toLowerCase() ?? '';
|
|
75
|
+
const rawText = el.innerText ?? '';
|
|
76
|
+
const text = rawText.slice(0, 80).trim();
|
|
77
|
+
sendEngagement(creds, 'click', 'engagement', {
|
|
78
|
+
tag,
|
|
79
|
+
...(text ? { text } : {}),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
document.addEventListener('click', onClick, { signal });
|
|
83
|
+
const focusedElements = new WeakSet();
|
|
84
|
+
function onFocusIn(e) {
|
|
85
|
+
const el = e.target;
|
|
86
|
+
if (!el)
|
|
87
|
+
return;
|
|
88
|
+
const tag = el.tagName?.toLowerCase() ?? '';
|
|
89
|
+
if (tag !== 'input' && tag !== 'textarea' && tag !== 'select')
|
|
90
|
+
return;
|
|
91
|
+
if (focusedElements.has(el))
|
|
92
|
+
return;
|
|
93
|
+
focusedElements.add(el);
|
|
94
|
+
sendEngagement(creds, 'input_focus', 'engagement', {
|
|
95
|
+
tag,
|
|
96
|
+
...(el.name ? { name: el.name } : {}),
|
|
97
|
+
...(el.type ? { type: el.type } : {}),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
document.addEventListener('focusin', onFocusIn, { signal });
|
|
101
|
+
function onVisibilityChange() {
|
|
102
|
+
if (document.visibilityState === 'hidden') {
|
|
103
|
+
flushScrollDepth();
|
|
104
|
+
if (activeSeconds > 0) {
|
|
105
|
+
sendEngagement(creds, 'session_ping', 'lifecycle', { active_seconds: activeSeconds });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
document.addEventListener('visibilitychange', onVisibilityChange, { signal });
|
|
110
|
+
function onBeforeUnload() {
|
|
111
|
+
flushScrollDepth();
|
|
112
|
+
sendEngagement(creds, 'session_end', 'lifecycle', { active_seconds: activeSeconds });
|
|
113
|
+
}
|
|
114
|
+
window.addEventListener('beforeunload', onBeforeUnload, { signal });
|
|
115
|
+
function sendKeepalive() {
|
|
116
|
+
if (document.visibilityState !== 'visible')
|
|
117
|
+
return;
|
|
118
|
+
sendEngagement(creds, 'heartbeat', 'lifecycle', { active_seconds: activeSeconds }, getSessionContextOnce());
|
|
119
|
+
}
|
|
120
|
+
sendKeepalive();
|
|
121
|
+
const keepaliveId = setInterval(sendKeepalive, KEEPALIVE_INTERVAL_MS);
|
|
122
|
+
return () => {
|
|
123
|
+
ac.abort();
|
|
124
|
+
if (scrollTimer)
|
|
125
|
+
clearTimeout(scrollTimer);
|
|
126
|
+
clearInterval(activeTickId);
|
|
127
|
+
clearInterval(keepaliveId);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -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,8 @@
|
|
|
1
|
+
import type { CollectCredentials } from '@nexly/core';
|
|
2
|
+
/** Current document path, or `/` outside a browser. */
|
|
3
|
+
export declare function getDefaultPathname(): string;
|
|
4
|
+
/**
|
|
5
|
+
* Page view: sends slim per-event context plus session context (upserted server-side).
|
|
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":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAIrD,uDAAuD;AACvD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,EAAE,IAAI,GAAE,MAA6B,GAAG,OAAO,CAU1G"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { sendBeaconCollect } from '../beacon.js';
|
|
2
|
+
import { collectEventMeta, collectSessionMeta } from '../browser-meta.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 per-event context plus session context (upserted server-side).
|
|
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: { ...collectEventMeta(), path },
|
|
17
|
+
data: {},
|
|
18
|
+
sessionContext: collectSessionMeta(),
|
|
19
|
+
});
|
|
20
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Nexly SDK (public API).
|
|
3
|
+
*/
|
|
4
|
+
export { Nexly } from './nexly.js';
|
|
5
|
+
export type { NexlyEventInput, NexlyEventType, NexlyInit } from '@nexly/core';
|
|
6
|
+
export { collectBrowserMeta, collectSessionMeta, collectEventMeta } from './browser-meta.js';
|
|
7
|
+
export { getSessionId, getVisitorId } from './session.js';
|
|
8
|
+
export { sendBeaconCollect, type SendBeaconCollectInput } from './beacon.js';
|
|
9
|
+
export { getDefaultPathname, sendPageViewBeacon, startEngagementTracking } from './events/index.js';
|
|
10
|
+
export type { CollectCredentials, IngestCredentials, TrackEventInput } from '@nexly/core';
|
|
11
|
+
export { buildCollectPayload } from '@nexly/core';
|
|
12
|
+
//# 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,MAAM,YAAY,CAAA;AAClC,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC7E,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAC5F,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAC5E,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAA;AACnG,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AACzF,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Nexly SDK (public API).
|
|
3
|
+
*/
|
|
4
|
+
export { Nexly } from './nexly.js';
|
|
5
|
+
export { collectBrowserMeta, collectSessionMeta, collectEventMeta } from './browser-meta.js';
|
|
6
|
+
export { getSessionId, getVisitorId } from './session.js';
|
|
7
|
+
export { sendBeaconCollect } from './beacon.js';
|
|
8
|
+
export { getDefaultPathname, sendPageViewBeacon, startEngagementTracking } from './events/index.js';
|
|
9
|
+
export { buildCollectPayload } from '@nexly/core';
|
package/dist/nexly.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NexlyBase, type NexlyInit } from '@nexly/core';
|
|
2
|
+
/**
|
|
3
|
+
* Browser-specific Nexly client. Implements transport via `navigator.sendBeacon`
|
|
4
|
+
* and metadata via DOM/Navigator APIs.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Nexly extends NexlyBase {
|
|
7
|
+
/** Creates (or replaces) the browser singleton. */
|
|
8
|
+
static init(options: NexlyInit): Nexly;
|
|
9
|
+
/** Returns the browser singleton, or `null` if not yet initialized. */
|
|
10
|
+
static getInstance(): Nexly | null;
|
|
11
|
+
constructor(init: NexlyInit);
|
|
12
|
+
protected transport(payload: Record<string, unknown>): boolean;
|
|
13
|
+
protected collectEventContext(): Record<string, unknown>;
|
|
14
|
+
protected collectSessionContext(): Record<string, unknown>;
|
|
15
|
+
getDefaultPath(): string;
|
|
16
|
+
startEngagement(): () => void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=nexly.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nexly.d.ts","sourceRoot":"","sources":["../src/nexly.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAA;AAMvD;;;GAGG;AACH,qBAAa,KAAM,SAAQ,SAAS;IAClC,mDAAmD;IACnD,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,KAAK;IAMtC,uEAAuE;IACvE,MAAM,CAAC,WAAW,IAAI,KAAK,GAAG,IAAI;gBAItB,IAAI,EAAE,SAAS;cAIR,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO;cAKpD,mBAAmB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;cAI9C,qBAAqB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAI1D,cAAc,IAAI,MAAM;IAIxB,eAAe,IAAI,MAAM,IAAI;CAQvC"}
|
package/dist/nexly.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { NexlyBase } from '@nexly/core';
|
|
2
|
+
import { sendPayloadBeacon } from './beacon.js';
|
|
3
|
+
import { collectEventMeta, collectSessionMeta } from './browser-meta.js';
|
|
4
|
+
import { startEngagementTracking } from './events/engagement.js';
|
|
5
|
+
import { getDefaultPathname } from './events/pageview.js';
|
|
6
|
+
/**
|
|
7
|
+
* Browser-specific Nexly client. Implements transport via `navigator.sendBeacon`
|
|
8
|
+
* and metadata via DOM/Navigator APIs.
|
|
9
|
+
*/
|
|
10
|
+
export class Nexly extends NexlyBase {
|
|
11
|
+
/** Creates (or replaces) the browser singleton. */
|
|
12
|
+
static init(options) {
|
|
13
|
+
const instance = new Nexly(options);
|
|
14
|
+
Nexly.setInstance(instance);
|
|
15
|
+
return instance;
|
|
16
|
+
}
|
|
17
|
+
/** Returns the browser singleton, or `null` if not yet initialized. */
|
|
18
|
+
static getInstance() {
|
|
19
|
+
return NexlyBase.getInstance() ?? null;
|
|
20
|
+
}
|
|
21
|
+
constructor(init) {
|
|
22
|
+
super(init);
|
|
23
|
+
}
|
|
24
|
+
transport(payload) {
|
|
25
|
+
if (!this.collectUrl)
|
|
26
|
+
return false;
|
|
27
|
+
return sendPayloadBeacon(this.collectUrl, payload);
|
|
28
|
+
}
|
|
29
|
+
collectEventContext() {
|
|
30
|
+
return collectEventMeta();
|
|
31
|
+
}
|
|
32
|
+
collectSessionContext() {
|
|
33
|
+
return collectSessionMeta();
|
|
34
|
+
}
|
|
35
|
+
getDefaultPath() {
|
|
36
|
+
return getDefaultPathname();
|
|
37
|
+
}
|
|
38
|
+
startEngagement() {
|
|
39
|
+
if (this.disabled)
|
|
40
|
+
return () => { };
|
|
41
|
+
return startEngagementTracking({
|
|
42
|
+
collectUrl: this.collectUrl,
|
|
43
|
+
appId: this.appId,
|
|
44
|
+
apiToken: this.key,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent anonymous visitor ID (localStorage). One per browser + origin.
|
|
3
|
+
*/
|
|
4
|
+
export declare function getVisitorId(): string;
|
|
5
|
+
/**
|
|
6
|
+
* Session ID with 30-minute inactivity timeout.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getSessionId(): string;
|
|
9
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAoBA;;GAEG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAUrC;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAkBrC"}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
*/
|
|
20
|
+
export function getVisitorId() {
|
|
21
|
+
if (typeof localStorage === 'undefined') {
|
|
22
|
+
return generateId();
|
|
23
|
+
}
|
|
24
|
+
let id = localStorage.getItem(VISITOR_KEY);
|
|
25
|
+
if (!id) {
|
|
26
|
+
id = generateId();
|
|
27
|
+
localStorage.setItem(VISITOR_KEY, id);
|
|
28
|
+
}
|
|
29
|
+
return id;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Session ID with 30-minute inactivity timeout.
|
|
33
|
+
*/
|
|
34
|
+
export function getSessionId() {
|
|
35
|
+
if (typeof sessionStorage === 'undefined') {
|
|
36
|
+
return generateId();
|
|
37
|
+
}
|
|
38
|
+
const existing = sessionStorage.getItem(SESSION_KEY);
|
|
39
|
+
const tsRaw = localStorage.getItem(SESSION_TS_KEY);
|
|
40
|
+
const lastTs = tsRaw ? Number(tsRaw) : 0;
|
|
41
|
+
const now = nowMs();
|
|
42
|
+
if (existing && lastTs && now - lastTs < SESSION_TIMEOUT_MS) {
|
|
43
|
+
localStorage.setItem(SESSION_TS_KEY, String(now));
|
|
44
|
+
return existing;
|
|
45
|
+
}
|
|
46
|
+
const id = generateId();
|
|
47
|
+
sessionStorage.setItem(SESSION_KEY, id);
|
|
48
|
+
localStorage.setItem(SESSION_TS_KEY, String(now));
|
|
49
|
+
return id;
|
|
50
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nexly/web",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Nexly browser ingest SDK: sendBeacon transport, DOM metadata, engagement listeners on top of @nexly/core",
|
|
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
|
+
"dependencies": {
|
|
41
|
+
"@nexly/core": "0.9.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"typescript": "~6.0.2"
|
|
45
|
+
}
|
|
46
|
+
}
|