@nexly/web 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/events/engagement.d.ts.map +1 -1
- package/dist/events/engagement.js +5 -1
- package/dist/events/index.d.ts +1 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +1 -0
- package/dist/events/outbound-links.d.ts +17 -0
- package/dist/events/outbound-links.d.ts.map +1 -0
- package/dist/events/outbound-links.js +188 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/nexly.d.ts +13 -0
- package/dist/nexly.d.ts.map +1 -1
- package/dist/nexly.js +38 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,6 +25,18 @@ const stopEngagement = client.startEngagement()
|
|
|
25
25
|
// stopEngagement() on SPA unmount / route change if needed
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
`startEngagement()` is idempotent — calling it again stops the previous subscription first, so HMR or double-mounts can't produce duplicate listeners.
|
|
29
|
+
|
|
30
|
+
### Opting elements out of auto-capture
|
|
31
|
+
|
|
32
|
+
Auto-click and auto-focus listeners skip any element (or descendant of an element) marked with `data-nexly-ignore`. Use it on elements where you already emit your own `event(...)` to avoid semantic duplicates:
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
<button data-nexly-ignore onClick={() => client.event({ name: 'signup_clicked', type: 'custom' })}>
|
|
36
|
+
Sign up
|
|
37
|
+
</button>
|
|
38
|
+
```
|
|
39
|
+
|
|
28
40
|
## What you get
|
|
29
41
|
|
|
30
42
|
- `Nexly` — browser client (auto session + visitor IDs via `localStorage`/`sessionStorage`).
|
|
@@ -55,3 +67,7 @@ await fetch('/api/link-visitor', {
|
|
|
55
67
|
`getVisitorId()` reads the persistent anonymous id Nexly stores in `localStorage`; `getSessionId()` reads the 30-minute tab session id from `sessionStorage`. Both are plain strings, safe to call from anywhere.
|
|
56
68
|
|
|
57
69
|
See the full recipe (backend storage + emitting linked events) in the [`@nexly/node` README](https://www.npmjs.com/package/@nexly/node).
|
|
70
|
+
|
|
71
|
+
## Bot detection
|
|
72
|
+
|
|
73
|
+
Bots and AI crawlers are classified server-side from the HTTP `User-Agent` header at ingest — do **not** try to compute or send an `is_bot` flag from your page code. Every event is stored (bots are not filtered out); the dashboard surfaces them in the dedicated "AI & Crawler activity" section and excludes them from standard visitor metrics.
|
|
@@ -1 +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;AA4BrD,KAAK,MAAM,GAAG,MAAM,IAAI,CAAA;AAExB;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"engagement.d.ts","sourceRoot":"","sources":["../../src/events/engagement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AA4BrD,KAAK,MAAM,GAAG,MAAM,IAAI,CAAA;AAExB;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,CAgHzE"}
|
|
@@ -68,10 +68,12 @@ export function startEngagementTracking(creds) {
|
|
|
68
68
|
const now = Date.now();
|
|
69
69
|
if (now - lastClickTs < CLICK_THROTTLE_MS)
|
|
70
70
|
return;
|
|
71
|
-
lastClickTs = now;
|
|
72
71
|
const el = e.target;
|
|
73
72
|
if (!el)
|
|
74
73
|
return;
|
|
74
|
+
if (el.closest('[data-nexly-ignore]'))
|
|
75
|
+
return;
|
|
76
|
+
lastClickTs = now;
|
|
75
77
|
const tag = el.tagName?.toLowerCase() ?? '';
|
|
76
78
|
const rawText = el.innerText ?? '';
|
|
77
79
|
const text = rawText.slice(0, 80).trim();
|
|
@@ -86,6 +88,8 @@ export function startEngagementTracking(creds) {
|
|
|
86
88
|
const el = e.target;
|
|
87
89
|
if (!el)
|
|
88
90
|
return;
|
|
91
|
+
if (el.closest('[data-nexly-ignore]'))
|
|
92
|
+
return;
|
|
89
93
|
const tag = el.tagName?.toLowerCase() ?? '';
|
|
90
94
|
if (tag !== 'input' && tag !== 'textarea' && tag !== 'select')
|
|
91
95
|
return;
|
package/dist/events/index.d.ts
CHANGED
|
@@ -1 +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"}
|
|
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,yBAAyB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA"}
|
package/dist/events/index.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CollectCredentials } from '@nexly/core';
|
|
2
|
+
type StopFn = () => void;
|
|
3
|
+
/**
|
|
4
|
+
* Attaches a delegated listener that auto-tracks four navigation cases on
|
|
5
|
+
* `<a>` elements:
|
|
6
|
+
* - external `http(s)` links — `outbound_link_clicked`
|
|
7
|
+
* - file downloads (by `download` attr or known extension) — `file_downloaded`
|
|
8
|
+
* - `mailto:` links — `mailto_clicked`
|
|
9
|
+
* - `tel:` links — `tel_clicked`
|
|
10
|
+
*
|
|
11
|
+
* All events use `type: 'navigation'`. Listens to both `click` and `auxclick`
|
|
12
|
+
* (middle-click). Skips elements (or their ancestors) marked with
|
|
13
|
+
* `[data-nexly-ignore]`. Returns a cleanup function.
|
|
14
|
+
*/
|
|
15
|
+
export declare function startOutboundLinkTracking(creds: CollectCredentials): StopFn;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=outbound-links.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbound-links.d.ts","sourceRoot":"","sources":["../../src/events/outbound-links.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAkKrD,KAAK,MAAM,GAAG,MAAM,IAAI,CAAA;AAExB;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,CA0C3E"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { sendBeaconCollect } from '../beacon.js';
|
|
2
|
+
import { collectEventMeta } from '../browser-meta.js';
|
|
3
|
+
/**
|
|
4
|
+
* File extensions treated as downloadable assets when no explicit `download`
|
|
5
|
+
* attribute is present on the link. Mirrors the defaults used by Plausible's
|
|
6
|
+
* `fileDownloads` extension, with a few common additions.
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_DOWNLOAD_EXTENSIONS = new Set([
|
|
9
|
+
'7z',
|
|
10
|
+
'avi',
|
|
11
|
+
'csv',
|
|
12
|
+
'dmg',
|
|
13
|
+
'doc',
|
|
14
|
+
'docx',
|
|
15
|
+
'exe',
|
|
16
|
+
'gz',
|
|
17
|
+
'key',
|
|
18
|
+
'mkv',
|
|
19
|
+
'mp3',
|
|
20
|
+
'mp4',
|
|
21
|
+
'mov',
|
|
22
|
+
'msi',
|
|
23
|
+
'numbers',
|
|
24
|
+
'odp',
|
|
25
|
+
'ods',
|
|
26
|
+
'odt',
|
|
27
|
+
'pages',
|
|
28
|
+
'pdf',
|
|
29
|
+
'pkg',
|
|
30
|
+
'ppt',
|
|
31
|
+
'pptx',
|
|
32
|
+
'rar',
|
|
33
|
+
'rtf',
|
|
34
|
+
'tar',
|
|
35
|
+
'tgz',
|
|
36
|
+
'txt',
|
|
37
|
+
'wav',
|
|
38
|
+
'webm',
|
|
39
|
+
'wma',
|
|
40
|
+
'xls',
|
|
41
|
+
'xlsx',
|
|
42
|
+
'xz',
|
|
43
|
+
'zip',
|
|
44
|
+
]);
|
|
45
|
+
const LINK_TEXT_MAX = 80;
|
|
46
|
+
const CLICK_THROTTLE_MS = 300;
|
|
47
|
+
function sendOutbound(creds, eventName, data) {
|
|
48
|
+
sendBeaconCollect({
|
|
49
|
+
collectUrl: creds.collectUrl,
|
|
50
|
+
credentials: { appId: creds.appId, apiToken: creds.apiToken },
|
|
51
|
+
client: creds.client,
|
|
52
|
+
eventName,
|
|
53
|
+
eventType: 'navigation',
|
|
54
|
+
context: collectEventMeta(),
|
|
55
|
+
data,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function getLinkText(link) {
|
|
59
|
+
const raw = link.innerText ?? link.textContent ?? '';
|
|
60
|
+
const trimmed = raw.replace(/\s+/g, ' ').trim().slice(0, LINK_TEXT_MAX);
|
|
61
|
+
return trimmed || undefined;
|
|
62
|
+
}
|
|
63
|
+
function getFileExtension(pathname) {
|
|
64
|
+
const lastSlash = pathname.lastIndexOf('/');
|
|
65
|
+
const fileName = lastSlash >= 0 ? pathname.slice(lastSlash + 1) : pathname;
|
|
66
|
+
const dot = fileName.lastIndexOf('.');
|
|
67
|
+
if (dot <= 0 || dot === fileName.length - 1)
|
|
68
|
+
return undefined;
|
|
69
|
+
return fileName.slice(dot + 1).toLowerCase();
|
|
70
|
+
}
|
|
71
|
+
function categorizeLink(link, hrefAttr, currentHost) {
|
|
72
|
+
let url;
|
|
73
|
+
try {
|
|
74
|
+
url = new URL(hrefAttr, window.location.href);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const linkText = getLinkText(link);
|
|
80
|
+
if (url.protocol === 'mailto:') {
|
|
81
|
+
const address = decodeURIComponent(url.pathname || hrefAttr.replace(/^mailto:/i, ''));
|
|
82
|
+
return {
|
|
83
|
+
eventName: 'mailto_clicked',
|
|
84
|
+
data: {
|
|
85
|
+
url: hrefAttr,
|
|
86
|
+
...(address ? { address } : {}),
|
|
87
|
+
...(linkText ? { link_text: linkText } : {}),
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (url.protocol === 'tel:') {
|
|
92
|
+
const number = decodeURIComponent(url.pathname || hrefAttr.replace(/^tel:/i, ''));
|
|
93
|
+
return {
|
|
94
|
+
eventName: 'tel_clicked',
|
|
95
|
+
data: {
|
|
96
|
+
url: hrefAttr,
|
|
97
|
+
...(number ? { number } : {}),
|
|
98
|
+
...(linkText ? { link_text: linkText } : {}),
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const isExternal = url.host !== currentHost;
|
|
106
|
+
const hasDownloadAttr = link.hasAttribute('download');
|
|
107
|
+
const ext = getFileExtension(url.pathname);
|
|
108
|
+
const isDownload = hasDownloadAttr || (ext !== undefined && DEFAULT_DOWNLOAD_EXTENSIONS.has(ext));
|
|
109
|
+
if (isDownload) {
|
|
110
|
+
const fileName = url.pathname.split('/').filter(Boolean).pop();
|
|
111
|
+
return {
|
|
112
|
+
eventName: 'file_downloaded',
|
|
113
|
+
data: {
|
|
114
|
+
url: url.href,
|
|
115
|
+
host: url.host,
|
|
116
|
+
...(fileName ? { file_name: fileName } : {}),
|
|
117
|
+
...(ext ? { file_extension: ext } : {}),
|
|
118
|
+
external: isExternal,
|
|
119
|
+
...(linkText ? { link_text: linkText } : {}),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (isExternal) {
|
|
124
|
+
return {
|
|
125
|
+
eventName: 'outbound_link_clicked',
|
|
126
|
+
data: {
|
|
127
|
+
url: url.href,
|
|
128
|
+
host: url.host,
|
|
129
|
+
...(linkText ? { link_text: linkText } : {}),
|
|
130
|
+
...(link.target ? { target: link.target } : {}),
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Attaches a delegated listener that auto-tracks four navigation cases on
|
|
138
|
+
* `<a>` elements:
|
|
139
|
+
* - external `http(s)` links — `outbound_link_clicked`
|
|
140
|
+
* - file downloads (by `download` attr or known extension) — `file_downloaded`
|
|
141
|
+
* - `mailto:` links — `mailto_clicked`
|
|
142
|
+
* - `tel:` links — `tel_clicked`
|
|
143
|
+
*
|
|
144
|
+
* All events use `type: 'navigation'`. Listens to both `click` and `auxclick`
|
|
145
|
+
* (middle-click). Skips elements (or their ancestors) marked with
|
|
146
|
+
* `[data-nexly-ignore]`. Returns a cleanup function.
|
|
147
|
+
*/
|
|
148
|
+
export function startOutboundLinkTracking(creds) {
|
|
149
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
150
|
+
return () => { };
|
|
151
|
+
}
|
|
152
|
+
const ac = new AbortController();
|
|
153
|
+
const { signal } = ac;
|
|
154
|
+
let lastDispatch = { href: '', ts: 0 };
|
|
155
|
+
function handle(e) {
|
|
156
|
+
if (e.defaultPrevented)
|
|
157
|
+
return;
|
|
158
|
+
if (e.type === 'click' && e.button !== 0 && e.button !== 1)
|
|
159
|
+
return;
|
|
160
|
+
if (e.type === 'auxclick' && e.button !== 1)
|
|
161
|
+
return;
|
|
162
|
+
const target = e.target;
|
|
163
|
+
if (!target)
|
|
164
|
+
return;
|
|
165
|
+
const link = target.closest('a');
|
|
166
|
+
if (!link)
|
|
167
|
+
return;
|
|
168
|
+
if (link.closest('[data-nexly-ignore]'))
|
|
169
|
+
return;
|
|
170
|
+
const hrefAttr = link.getAttribute('href');
|
|
171
|
+
if (!hrefAttr)
|
|
172
|
+
return;
|
|
173
|
+
const category = categorizeLink(link, hrefAttr, window.location.host);
|
|
174
|
+
if (!category)
|
|
175
|
+
return;
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
if (lastDispatch.href === hrefAttr && now - lastDispatch.ts < CLICK_THROTTLE_MS) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
lastDispatch = { href: hrefAttr, ts: now };
|
|
181
|
+
sendOutbound(creds, category.eventName, category.data);
|
|
182
|
+
}
|
|
183
|
+
document.addEventListener('click', handle, { signal, capture: true });
|
|
184
|
+
document.addEventListener('auxclick', handle, { signal, capture: true });
|
|
185
|
+
return () => {
|
|
186
|
+
ac.abort();
|
|
187
|
+
};
|
|
188
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type { NexlyEventInput, NexlyEventType, NexlyInit } from '@nexly/core';
|
|
|
6
6
|
export { collectBrowserMeta, collectSessionMeta, collectEventMeta } from './browser-meta.js';
|
|
7
7
|
export { getSessionId, getVisitorId } from './session.js';
|
|
8
8
|
export { sendBeaconCollect, type SendBeaconCollectInput } from './beacon.js';
|
|
9
|
-
export { getDefaultPathname, sendPageViewBeacon, startEngagementTracking } from './events/index.js';
|
|
9
|
+
export { getDefaultPathname, sendPageViewBeacon, startEngagementTracking, startOutboundLinkTracking, } from './events/index.js';
|
|
10
10
|
export type { CollectCredentials, IngestCredentials, TrackEventInput } from '@nexly/core';
|
|
11
11
|
export { buildCollectPayload } from '@nexly/core';
|
|
12
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
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,EACL,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AACzF,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -5,5 +5,5 @@ export { Nexly } from './nexly.js';
|
|
|
5
5
|
export { collectBrowserMeta, collectSessionMeta, collectEventMeta } from './browser-meta.js';
|
|
6
6
|
export { getSessionId, getVisitorId } from './session.js';
|
|
7
7
|
export { sendBeaconCollect } from './beacon.js';
|
|
8
|
-
export { getDefaultPathname, sendPageViewBeacon, startEngagementTracking } from './events/index.js';
|
|
8
|
+
export { getDefaultPathname, sendPageViewBeacon, startEngagementTracking, startOutboundLinkTracking, } from './events/index.js';
|
|
9
9
|
export { buildCollectPayload } from '@nexly/core';
|
package/dist/nexly.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { NexlyBase, type NexlyInit } from '@nexly/core';
|
|
|
4
4
|
* and metadata via DOM/Navigator APIs.
|
|
5
5
|
*/
|
|
6
6
|
export declare class Nexly extends NexlyBase {
|
|
7
|
+
private engagementStop;
|
|
8
|
+
private outboundLinksStop;
|
|
7
9
|
/** Creates (or replaces) the browser singleton. Default `client` is `NexlyClient.Web`. */
|
|
8
10
|
static init(options: NexlyInit): Nexly;
|
|
9
11
|
/** Returns the browser singleton, or `null` if not yet initialized. */
|
|
@@ -13,6 +15,17 @@ export declare class Nexly extends NexlyBase {
|
|
|
13
15
|
protected collectEventContext(): Record<string, unknown>;
|
|
14
16
|
protected collectSessionContext(): Record<string, unknown>;
|
|
15
17
|
getDefaultPath(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Attaches engagement listeners. Idempotent: calling again stops the previous
|
|
20
|
+
* subscription first, so HMR / double-mounts can't produce duplicate listeners.
|
|
21
|
+
* Returns a cleanup function.
|
|
22
|
+
*/
|
|
16
23
|
startEngagement(): () => void;
|
|
24
|
+
/**
|
|
25
|
+
* Attaches a delegated click listener that auto-tracks outbound links,
|
|
26
|
+
* file downloads, `mailto:` and `tel:` clicks. Idempotent: re-calling stops
|
|
27
|
+
* the previous subscription first. Returns a cleanup function.
|
|
28
|
+
*/
|
|
29
|
+
startOutboundLinks(): () => void;
|
|
17
30
|
}
|
|
18
31
|
//# sourceMappingURL=nexly.d.ts.map
|
package/dist/nexly.d.ts.map
CHANGED
|
@@ -1 +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;
|
|
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;AAOpE;;;GAGG;AACH,qBAAa,KAAM,SAAQ,SAAS;IAClC,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,iBAAiB,CAA4B;IAErD,0FAA0F;IAC1F,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;IAIjC;;;;OAIG;IACH,eAAe,IAAI,MAAM,IAAI;IAgB7B;;;;OAIG;IACH,kBAAkB,IAAI,MAAM,IAAI;CAejC"}
|
package/dist/nexly.js
CHANGED
|
@@ -2,12 +2,15 @@ import { NexlyBase, NexlyClient } from '@nexly/core';
|
|
|
2
2
|
import { sendPayloadBeacon } from './beacon.js';
|
|
3
3
|
import { collectEventMeta, collectSessionMeta } from './browser-meta.js';
|
|
4
4
|
import { startEngagementTracking } from './events/engagement.js';
|
|
5
|
+
import { startOutboundLinkTracking } from './events/outbound-links.js';
|
|
5
6
|
import { getDefaultPathname } from './events/pageview.js';
|
|
6
7
|
/**
|
|
7
8
|
* Browser-specific Nexly client. Implements transport via `navigator.sendBeacon`
|
|
8
9
|
* and metadata via DOM/Navigator APIs.
|
|
9
10
|
*/
|
|
10
11
|
export class Nexly extends NexlyBase {
|
|
12
|
+
engagementStop = null;
|
|
13
|
+
outboundLinksStop = null;
|
|
11
14
|
/** Creates (or replaces) the browser singleton. Default `client` is `NexlyClient.Web`. */
|
|
12
15
|
static init(options) {
|
|
13
16
|
const instance = new Nexly({ ...options, client: options.client ?? NexlyClient.Web });
|
|
@@ -35,14 +38,48 @@ export class Nexly extends NexlyBase {
|
|
|
35
38
|
getDefaultPath() {
|
|
36
39
|
return getDefaultPathname();
|
|
37
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Attaches engagement listeners. Idempotent: calling again stops the previous
|
|
43
|
+
* subscription first, so HMR / double-mounts can't produce duplicate listeners.
|
|
44
|
+
* Returns a cleanup function.
|
|
45
|
+
*/
|
|
38
46
|
startEngagement() {
|
|
39
47
|
if (this.disabled)
|
|
40
48
|
return () => { };
|
|
41
|
-
|
|
49
|
+
this.engagementStop?.();
|
|
50
|
+
const stop = startEngagementTracking({
|
|
42
51
|
collectUrl: this.collectUrl,
|
|
43
52
|
appId: this.appId,
|
|
44
53
|
apiToken: this.key,
|
|
45
54
|
client: this.client,
|
|
46
55
|
});
|
|
56
|
+
this.engagementStop = stop;
|
|
57
|
+
return () => {
|
|
58
|
+
stop();
|
|
59
|
+
if (this.engagementStop === stop)
|
|
60
|
+
this.engagementStop = null;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Attaches a delegated click listener that auto-tracks outbound links,
|
|
65
|
+
* file downloads, `mailto:` and `tel:` clicks. Idempotent: re-calling stops
|
|
66
|
+
* the previous subscription first. Returns a cleanup function.
|
|
67
|
+
*/
|
|
68
|
+
startOutboundLinks() {
|
|
69
|
+
if (this.disabled)
|
|
70
|
+
return () => { };
|
|
71
|
+
this.outboundLinksStop?.();
|
|
72
|
+
const stop = startOutboundLinkTracking({
|
|
73
|
+
collectUrl: this.collectUrl,
|
|
74
|
+
appId: this.appId,
|
|
75
|
+
apiToken: this.key,
|
|
76
|
+
client: this.client,
|
|
77
|
+
});
|
|
78
|
+
this.outboundLinksStop = stop;
|
|
79
|
+
return () => {
|
|
80
|
+
stop();
|
|
81
|
+
if (this.outboundLinksStop === stop)
|
|
82
|
+
this.outboundLinksStop = null;
|
|
83
|
+
};
|
|
47
84
|
}
|
|
48
85
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexly/web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Nexly browser ingest SDK: sendBeacon transport, DOM metadata, engagement listeners on top of @nexly/core",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"node": ">=20"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@nexly/core": "0.
|
|
41
|
+
"@nexly/core": "0.15.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"typescript": "~6.0.2"
|