@agentuity/analytics 3.0.0-alpha.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 +116 -0
- package/dist/beacon.d.ts +12 -0
- package/dist/beacon.d.ts.map +1 -0
- package/dist/beacon.js +300 -0
- package/dist/beacon.js.map +1 -0
- package/dist/client.d.ts +46 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +171 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +33 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +92 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +37 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +125 -0
- package/dist/util.js.map +1 -0
- package/package.json +54 -0
- package/src/beacon.ts +345 -0
- package/src/client.ts +189 -0
- package/src/config.ts +48 -0
- package/src/index.ts +124 -0
- package/src/types.ts +126 -0
- package/src/util.ts +123 -0
package/src/beacon.ts
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics beacon - auto-initializing script
|
|
3
|
+
*
|
|
4
|
+
* Import this module to automatically start tracking:
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import '@agentuity/analytics/beacon';
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* Requires window.__AGENTUITY_ANALYTICS__ to be set by server.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { isEnabled, getConfig, isDevmode } from './config';
|
|
13
|
+
import {
|
|
14
|
+
initClient,
|
|
15
|
+
updatePageView,
|
|
16
|
+
resetSession,
|
|
17
|
+
send,
|
|
18
|
+
track,
|
|
19
|
+
setupGlobal,
|
|
20
|
+
getPageView,
|
|
21
|
+
} from './client';
|
|
22
|
+
import { generateId, stripQueryString, getUTMParams, fetchGeo } from './util';
|
|
23
|
+
import type { PageViewData } from './types';
|
|
24
|
+
|
|
25
|
+
// Track if already initialized
|
|
26
|
+
let initialized = false;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize page view data
|
|
30
|
+
*/
|
|
31
|
+
function initPageView(): PageViewData {
|
|
32
|
+
const pv: PageViewData = {
|
|
33
|
+
id: generateId(),
|
|
34
|
+
timestamp: Date.now(),
|
|
35
|
+
timezone_offset: new Date().getTimezoneOffset(),
|
|
36
|
+
url: stripQueryString(location.href),
|
|
37
|
+
path: location.pathname,
|
|
38
|
+
referrer: stripQueryString(document.referrer),
|
|
39
|
+
title: document.title || '',
|
|
40
|
+
screen_width: screen.width || 0,
|
|
41
|
+
screen_height: screen.height || 0,
|
|
42
|
+
viewport_width: innerWidth || 0,
|
|
43
|
+
viewport_height: innerHeight || 0,
|
|
44
|
+
device_pixel_ratio: devicePixelRatio || 1,
|
|
45
|
+
user_agent: navigator.userAgent || '',
|
|
46
|
+
language: navigator.language || '',
|
|
47
|
+
scroll_depth: 0,
|
|
48
|
+
time_on_page: 0,
|
|
49
|
+
scroll_events: [],
|
|
50
|
+
custom_events: [],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Add UTM params
|
|
54
|
+
const utm = getUTMParams();
|
|
55
|
+
for (const k in utm) {
|
|
56
|
+
pv[k] = utm[k];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Capture navigation timing
|
|
60
|
+
if (typeof performance !== 'undefined' && performance.getEntriesByType) {
|
|
61
|
+
const nav = performance.getEntriesByType('navigation')[0] as
|
|
62
|
+
| PerformanceNavigationTiming
|
|
63
|
+
| undefined;
|
|
64
|
+
if (nav) {
|
|
65
|
+
pv.dom_ready = Math.round(nav.domContentLoadedEventEnd - nav.startTime);
|
|
66
|
+
pv.ttfb = Math.round(nav.responseStart - nav.requestStart);
|
|
67
|
+
if (nav.loadEventEnd > 0) {
|
|
68
|
+
pv.load_time = Math.round(nav.loadEventEnd - nav.startTime);
|
|
69
|
+
} else {
|
|
70
|
+
// Defer reading loadEventEnd
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
const navAfter = performance.getEntriesByType('navigation')[0] as
|
|
73
|
+
| PerformanceNavigationTiming
|
|
74
|
+
| undefined;
|
|
75
|
+
if (navAfter && navAfter.loadEventEnd > 0) {
|
|
76
|
+
updatePageView({
|
|
77
|
+
load_time: Math.round(navAfter.loadEventEnd - navAfter.startTime),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}, 0);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return pv;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Set up visibility change handlers
|
|
90
|
+
*/
|
|
91
|
+
function setupVisibilityHandlers(): void {
|
|
92
|
+
document.addEventListener('visibilitychange', () => {
|
|
93
|
+
if (document.visibilityState === 'hidden') {
|
|
94
|
+
send();
|
|
95
|
+
} else if (document.visibilityState === 'visible') {
|
|
96
|
+
// User returned - start new attention session
|
|
97
|
+
resetSession();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
window.addEventListener('pagehide', () => send());
|
|
102
|
+
window.addEventListener('beforeunload', () => send());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Set up scroll tracking
|
|
107
|
+
*/
|
|
108
|
+
function setupScrollTracking(): void {
|
|
109
|
+
const config = getConfig();
|
|
110
|
+
if (config?.trackScroll === false) return;
|
|
111
|
+
|
|
112
|
+
const scrolled = new Set<number>();
|
|
113
|
+
|
|
114
|
+
function getScrollDepth(): number {
|
|
115
|
+
const st = window.scrollY || document.documentElement.scrollTop;
|
|
116
|
+
const sh = document.documentElement.scrollHeight - document.documentElement.clientHeight;
|
|
117
|
+
return sh <= 0 ? 100 : Math.min(100, Math.round((st / sh) * 100));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
window.addEventListener(
|
|
121
|
+
'scroll',
|
|
122
|
+
() => {
|
|
123
|
+
const depth = getScrollDepth();
|
|
124
|
+
updatePageView({ scroll_depth: depth });
|
|
125
|
+
|
|
126
|
+
for (const m of [25, 50, 75, 100]) {
|
|
127
|
+
if (depth >= m && !scrolled.has(m)) {
|
|
128
|
+
scrolled.add(m);
|
|
129
|
+
const pv = getPageView();
|
|
130
|
+
if (pv) {
|
|
131
|
+
pv.scroll_events.push({
|
|
132
|
+
depth: m,
|
|
133
|
+
timestamp: Date.now(),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{ passive: true }
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Set up Web Vitals tracking
|
|
145
|
+
*/
|
|
146
|
+
function setupWebVitals(): void {
|
|
147
|
+
const config = getConfig();
|
|
148
|
+
if (config?.trackWebVitals === false) return;
|
|
149
|
+
if (typeof PerformanceObserver === 'undefined') return;
|
|
150
|
+
|
|
151
|
+
// FCP
|
|
152
|
+
try {
|
|
153
|
+
const fcpObs = new PerformanceObserver((list) => {
|
|
154
|
+
for (const entry of list.getEntries()) {
|
|
155
|
+
if (entry.name === 'first-contentful-paint') {
|
|
156
|
+
updatePageView({ fcp: Math.round(entry.startTime) });
|
|
157
|
+
fcpObs.disconnect();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
fcpObs.observe({ type: 'paint', buffered: true });
|
|
162
|
+
} catch {
|
|
163
|
+
/* Not supported */
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// LCP
|
|
167
|
+
try {
|
|
168
|
+
new PerformanceObserver((list) => {
|
|
169
|
+
const entries = list.getEntries();
|
|
170
|
+
const last = entries[entries.length - 1];
|
|
171
|
+
if (last) {
|
|
172
|
+
updatePageView({ lcp: Math.round(last.startTime) });
|
|
173
|
+
}
|
|
174
|
+
}).observe({ type: 'largest-contentful-paint', buffered: true });
|
|
175
|
+
} catch {
|
|
176
|
+
/* Not supported */
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// CLS
|
|
180
|
+
try {
|
|
181
|
+
let clsValue = 0;
|
|
182
|
+
new PerformanceObserver((list) => {
|
|
183
|
+
for (const entry of list.getEntries()) {
|
|
184
|
+
const shift = entry as PerformanceEntry & { hadRecentInput?: boolean; value?: number };
|
|
185
|
+
if (!shift.hadRecentInput && shift.value) {
|
|
186
|
+
clsValue += shift.value;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
updatePageView({ cls: Math.round(clsValue * 1000) / 1000 });
|
|
190
|
+
}).observe({ type: 'layout-shift', buffered: true });
|
|
191
|
+
} catch {
|
|
192
|
+
/* Not supported */
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// INP
|
|
196
|
+
try {
|
|
197
|
+
let inpValue = 0;
|
|
198
|
+
new PerformanceObserver((list) => {
|
|
199
|
+
for (const entry of list.getEntries()) {
|
|
200
|
+
const event = entry as PerformanceEntry & { duration?: number };
|
|
201
|
+
if (event.duration && event.duration > inpValue) {
|
|
202
|
+
inpValue = event.duration;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
updatePageView({ inp: Math.round(inpValue) });
|
|
206
|
+
}).observe({ type: 'event', buffered: true });
|
|
207
|
+
} catch {
|
|
208
|
+
/* Not supported */
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Set up SPA navigation tracking
|
|
214
|
+
*/
|
|
215
|
+
function setupSPANavigation(): void {
|
|
216
|
+
const config = getConfig();
|
|
217
|
+
if (config?.trackSPANavigation === false) return;
|
|
218
|
+
|
|
219
|
+
let currentPath = location.pathname + location.search;
|
|
220
|
+
let lastHref = location.href;
|
|
221
|
+
|
|
222
|
+
function handleNav(): void {
|
|
223
|
+
const newPath = location.pathname + location.search;
|
|
224
|
+
if (newPath !== currentPath) {
|
|
225
|
+
send(true); // Force send on SPA nav
|
|
226
|
+
currentPath = newPath;
|
|
227
|
+
lastHref = location.href;
|
|
228
|
+
initClient(initPageView());
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Monkey-patch history
|
|
233
|
+
const origPush = history.pushState;
|
|
234
|
+
const origReplace = history.replaceState;
|
|
235
|
+
|
|
236
|
+
history.pushState = function (...args: [data: unknown, unused: string, url?: string | URL]) {
|
|
237
|
+
origPush.apply(this, args);
|
|
238
|
+
setTimeout(handleNav, 0);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
history.replaceState = function (...args: [data: unknown, unused: string, url?: string | URL]) {
|
|
242
|
+
origReplace.apply(this, args);
|
|
243
|
+
setTimeout(handleNav, 0);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
window.addEventListener('popstate', handleNav);
|
|
247
|
+
|
|
248
|
+
// Fallback: poll for URL changes
|
|
249
|
+
setInterval(() => {
|
|
250
|
+
if (location.href !== lastHref) {
|
|
251
|
+
lastHref = location.href;
|
|
252
|
+
handleNav();
|
|
253
|
+
}
|
|
254
|
+
}, 200);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Set up click tracking
|
|
259
|
+
*/
|
|
260
|
+
function setupClickTracking(): void {
|
|
261
|
+
const config = getConfig();
|
|
262
|
+
if (config?.trackClicks === false) return;
|
|
263
|
+
|
|
264
|
+
document.addEventListener(
|
|
265
|
+
'click',
|
|
266
|
+
(e) => {
|
|
267
|
+
const target = e.target as Element | null;
|
|
268
|
+
if (!target) return;
|
|
269
|
+
|
|
270
|
+
const el = target.closest('[data-analytics]');
|
|
271
|
+
if (!el) return;
|
|
272
|
+
|
|
273
|
+
const name = 'click:' + el.getAttribute('data-analytics');
|
|
274
|
+
track(name);
|
|
275
|
+
},
|
|
276
|
+
true
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Set up error tracking
|
|
282
|
+
*/
|
|
283
|
+
function setupErrorTracking(): void {
|
|
284
|
+
const config = getConfig();
|
|
285
|
+
if (config?.trackErrors === false) return;
|
|
286
|
+
|
|
287
|
+
window.addEventListener('error', (e) => {
|
|
288
|
+
track('error:js_error', {
|
|
289
|
+
message: e.message || 'Unknown',
|
|
290
|
+
filename: e.filename || '',
|
|
291
|
+
lineno: e.lineno || 0,
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
window.addEventListener('unhandledrejection', (e) => {
|
|
296
|
+
track('error:unhandled_rejection', {
|
|
297
|
+
message: e.reason instanceof Error ? e.reason.message : String(e.reason),
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Initialize the beacon
|
|
304
|
+
*/
|
|
305
|
+
function init(): void {
|
|
306
|
+
if (initialized) return;
|
|
307
|
+
if (!isEnabled()) return;
|
|
308
|
+
|
|
309
|
+
initialized = true;
|
|
310
|
+
|
|
311
|
+
// Init page view
|
|
312
|
+
const pv = initPageView();
|
|
313
|
+
initClient(pv);
|
|
314
|
+
|
|
315
|
+
// Fetch geo (async)
|
|
316
|
+
fetchGeo();
|
|
317
|
+
|
|
318
|
+
// Set up all tracking
|
|
319
|
+
setupVisibilityHandlers();
|
|
320
|
+
setupScrollTracking();
|
|
321
|
+
setupWebVitals();
|
|
322
|
+
setupSPANavigation();
|
|
323
|
+
setupClickTracking();
|
|
324
|
+
setupErrorTracking();
|
|
325
|
+
|
|
326
|
+
// Set up global API
|
|
327
|
+
setupGlobal();
|
|
328
|
+
|
|
329
|
+
// Init on load if not ready
|
|
330
|
+
if (document.readyState === 'complete') {
|
|
331
|
+
// Already loaded
|
|
332
|
+
} else {
|
|
333
|
+
window.addEventListener('load', () => {
|
|
334
|
+
// Re-capture timing after load
|
|
335
|
+
updatePageView(initPageView());
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (isDevmode()) {
|
|
340
|
+
console.debug('[Agentuity Analytics] Beacon initialized');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Auto-initialize on import
|
|
345
|
+
init();
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics client - programmatic API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { isEnabled, getConfig, getEndpoint } from './config';
|
|
6
|
+
import { generateId, safeStringify, getVisitorId } from './util';
|
|
7
|
+
import type { AnalyticsClient, PageViewData, AnalyticsPayload } from './types';
|
|
8
|
+
|
|
9
|
+
/** Pending custom events */
|
|
10
|
+
let customEvents: Array<{ timestamp: number; name: string; data: string }> = [];
|
|
11
|
+
|
|
12
|
+
/** Current user ID */
|
|
13
|
+
let userId = '';
|
|
14
|
+
|
|
15
|
+
/** Current user traits */
|
|
16
|
+
let userTraits: Record<string, string> = {};
|
|
17
|
+
|
|
18
|
+
/** Current page view data */
|
|
19
|
+
let pageView: PageViewData | null = null;
|
|
20
|
+
|
|
21
|
+
/** Whether current page view was sent */
|
|
22
|
+
let sent = false;
|
|
23
|
+
|
|
24
|
+
/** Page view start time */
|
|
25
|
+
let pageStart = Date.now();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Initialize client with page view data
|
|
29
|
+
* Called by beacon or can be called manually
|
|
30
|
+
*/
|
|
31
|
+
export function initClient(pv: PageViewData): void {
|
|
32
|
+
pageView = pv;
|
|
33
|
+
customEvents = [];
|
|
34
|
+
sent = false;
|
|
35
|
+
pageStart = Date.now();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Update page view data
|
|
40
|
+
*/
|
|
41
|
+
export function updatePageView(updates: Partial<PageViewData>): void {
|
|
42
|
+
if (pageView) {
|
|
43
|
+
Object.assign(pageView, updates);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get current page view data
|
|
49
|
+
*/
|
|
50
|
+
export function getPageView(): PageViewData | null {
|
|
51
|
+
return pageView;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Reset session (keep page-level metrics, reset session metrics)
|
|
56
|
+
*/
|
|
57
|
+
export function resetSession(): void {
|
|
58
|
+
if (pageView) {
|
|
59
|
+
pageView.id = generateId();
|
|
60
|
+
pageView.timestamp = Date.now();
|
|
61
|
+
pageView.scroll_events = [];
|
|
62
|
+
pageView.custom_events = customEvents;
|
|
63
|
+
pageView.scroll_depth = 0;
|
|
64
|
+
pageView.time_on_page = 0;
|
|
65
|
+
}
|
|
66
|
+
sent = false;
|
|
67
|
+
pageStart = Date.now();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build payload for sending
|
|
72
|
+
*/
|
|
73
|
+
function buildPayload(): AnalyticsPayload | null {
|
|
74
|
+
if (!pageView) return null;
|
|
75
|
+
|
|
76
|
+
const config = getConfig();
|
|
77
|
+
if (!config) return null;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
org_id: config.orgId,
|
|
81
|
+
project_id: config.projectId,
|
|
82
|
+
visitor_id: getVisitorId(),
|
|
83
|
+
user_id: userId,
|
|
84
|
+
user_traits: userTraits,
|
|
85
|
+
is_devmode: config.isDevmode ?? false,
|
|
86
|
+
pageview: {
|
|
87
|
+
...pageView,
|
|
88
|
+
custom_events: customEvents,
|
|
89
|
+
time_on_page: Date.now() - pageStart,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Send analytics data
|
|
96
|
+
*/
|
|
97
|
+
export function send(force = false): void {
|
|
98
|
+
if (sent && !force) return;
|
|
99
|
+
if (!isEnabled()) return;
|
|
100
|
+
|
|
101
|
+
const config = getConfig();
|
|
102
|
+
if (!config) return;
|
|
103
|
+
|
|
104
|
+
// Check sample rate
|
|
105
|
+
if (config.sampleRate !== undefined && config.sampleRate < 1) {
|
|
106
|
+
if (Math.random() > config.sampleRate) return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
sent = true;
|
|
110
|
+
|
|
111
|
+
const payload = buildPayload();
|
|
112
|
+
if (!payload) return;
|
|
113
|
+
|
|
114
|
+
// Dev mode: just log
|
|
115
|
+
if (config.isDevmode) {
|
|
116
|
+
console.debug('[Agentuity Analytics]', JSON.stringify(payload, null, 2));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Production: send to endpoint
|
|
121
|
+
const body = JSON.stringify(payload);
|
|
122
|
+
const endpoint = getEndpoint();
|
|
123
|
+
|
|
124
|
+
if (navigator.sendBeacon) {
|
|
125
|
+
navigator.sendBeacon(endpoint, body);
|
|
126
|
+
} else {
|
|
127
|
+
fetch(endpoint, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
body,
|
|
130
|
+
keepalive: true,
|
|
131
|
+
}).catch(() => {
|
|
132
|
+
// Silent failure
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Track a custom event
|
|
139
|
+
*/
|
|
140
|
+
export function track(name: string, properties?: Record<string, unknown>): void {
|
|
141
|
+
if (!isEnabled()) return;
|
|
142
|
+
if (customEvents.length >= 1000) return;
|
|
143
|
+
|
|
144
|
+
customEvents.push({
|
|
145
|
+
timestamp: Date.now(),
|
|
146
|
+
name,
|
|
147
|
+
data: safeStringify(properties),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Identify a user
|
|
153
|
+
*/
|
|
154
|
+
export function identify(id: string, traits?: Record<string, unknown>): void {
|
|
155
|
+
userId = id;
|
|
156
|
+
if (traits) {
|
|
157
|
+
userTraits = {};
|
|
158
|
+
for (const [key, value] of Object.entries(traits)) {
|
|
159
|
+
userTraits[key] = String(value);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Flush pending events
|
|
166
|
+
*/
|
|
167
|
+
export function flush(): void {
|
|
168
|
+
send(true);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get the analytics client
|
|
173
|
+
*/
|
|
174
|
+
export function getClient(): AnalyticsClient {
|
|
175
|
+
return {
|
|
176
|
+
track,
|
|
177
|
+
identify,
|
|
178
|
+
flush,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Set up client as window.global
|
|
184
|
+
*/
|
|
185
|
+
export function setupGlobal(): void {
|
|
186
|
+
if (typeof window !== 'undefined') {
|
|
187
|
+
window.agentuityAnalytics = getClient();
|
|
188
|
+
}
|
|
189
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics configuration resolution
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AnalyticsConfig } from './types';
|
|
6
|
+
|
|
7
|
+
/** Window with Agentuity analytics globals */
|
|
8
|
+
declare global {
|
|
9
|
+
interface Window {
|
|
10
|
+
__AGENTUITY_ANALYTICS__?: AnalyticsConfig;
|
|
11
|
+
agentuityAnalytics?: import('./types').AnalyticsClient;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Default collect endpoint */
|
|
16
|
+
export const DEFAULT_ENDPOINT = '/_agentuity/webanalytics/collect';
|
|
17
|
+
|
|
18
|
+
/** Maximum custom events per page view */
|
|
19
|
+
export const MAX_CUSTOM_EVENTS = 1000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get analytics config from window global
|
|
23
|
+
*/
|
|
24
|
+
export function getConfig(): AnalyticsConfig | null {
|
|
25
|
+
return window.__AGENTUITY_ANALYTICS__ ?? null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if analytics is enabled
|
|
30
|
+
*/
|
|
31
|
+
export function isEnabled(): boolean {
|
|
32
|
+
const config = getConfig();
|
|
33
|
+
return config?.enabled === true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if running in dev mode
|
|
38
|
+
*/
|
|
39
|
+
export function isDevmode(): boolean {
|
|
40
|
+
return getConfig()?.isDevmode ?? false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the collect endpoint
|
|
45
|
+
*/
|
|
46
|
+
export function getEndpoint(): string {
|
|
47
|
+
return getConfig()?.endpoint ?? DEFAULT_ENDPOINT;
|
|
48
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agentuity/analytics - Browser analytics for Agentuity applications
|
|
3
|
+
*
|
|
4
|
+
* ## Usage
|
|
5
|
+
*
|
|
6
|
+
* ### Auto-init (drop-in)
|
|
7
|
+
* ```typescript
|
|
8
|
+
* // Just import - uses window.__AGENTUITY_ANALYTICS__ config
|
|
9
|
+
* import '@agentuity/analytics/beacon';
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* ### Programmatic
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { init, track, identify, flush } from '@agentuity/analytics';
|
|
15
|
+
*
|
|
16
|
+
* init({
|
|
17
|
+
* orgId: 'your-org-id',
|
|
18
|
+
* projectId: 'your-project-id',
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* track('button_click', { button: 'signup' });
|
|
22
|
+
* identify('user-123', { email: 'user@example.com' });
|
|
23
|
+
* flush();
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* ### With React
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import { useEffect } from 'react';
|
|
29
|
+
* import { track } from '@agentuity/analytics';
|
|
30
|
+
*
|
|
31
|
+
* function SignupButton() {
|
|
32
|
+
* const handleClick = () => {
|
|
33
|
+
* track('signup_click');
|
|
34
|
+
* };
|
|
35
|
+
* return <button onClick={handleClick}>Sign Up</button>;
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
// Types
|
|
41
|
+
export type {
|
|
42
|
+
AnalyticsConfig,
|
|
43
|
+
AnalyticsClient,
|
|
44
|
+
AnalyticsPayload,
|
|
45
|
+
PageViewData,
|
|
46
|
+
ScrollEvent,
|
|
47
|
+
AnalyticsCustomEvent,
|
|
48
|
+
GeoLocation,
|
|
49
|
+
} from './types';
|
|
50
|
+
|
|
51
|
+
// Config utilities
|
|
52
|
+
export {
|
|
53
|
+
getConfig,
|
|
54
|
+
isEnabled,
|
|
55
|
+
isDevmode,
|
|
56
|
+
getEndpoint,
|
|
57
|
+
DEFAULT_ENDPOINT,
|
|
58
|
+
} from './config';
|
|
59
|
+
|
|
60
|
+
// Utility functions
|
|
61
|
+
export {
|
|
62
|
+
generateId,
|
|
63
|
+
getVisitorId,
|
|
64
|
+
getUTMParams,
|
|
65
|
+
stripQueryString,
|
|
66
|
+
} from './util';
|
|
67
|
+
|
|
68
|
+
// Programmatic API
|
|
69
|
+
export {
|
|
70
|
+
initClient,
|
|
71
|
+
track,
|
|
72
|
+
identify,
|
|
73
|
+
flush,
|
|
74
|
+
send,
|
|
75
|
+
getClient,
|
|
76
|
+
} from './client';
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Initialize analytics with explicit config
|
|
80
|
+
* Alternative to using window.__AGENTUITY_ANALYTICS__
|
|
81
|
+
*/
|
|
82
|
+
export function init(config: import('./types').AnalyticsConfig): void {
|
|
83
|
+
if (typeof window !== 'undefined') {
|
|
84
|
+
window.__AGENTUITY_ANALYTICS__ = config;
|
|
85
|
+
// Import beacon to trigger auto-init
|
|
86
|
+
import('./beacon');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get the global analytics client
|
|
92
|
+
*/
|
|
93
|
+
export function getAnalytics(): import('./types').AnalyticsClient | null {
|
|
94
|
+
if (typeof window !== 'undefined' && window.agentuityAnalytics) {
|
|
95
|
+
return window.agentuityAnalytics;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if user has opted out of analytics via localStorage.
|
|
102
|
+
*/
|
|
103
|
+
export function isOptedOut(): boolean {
|
|
104
|
+
try {
|
|
105
|
+
return localStorage.getItem('agentuity_opt_out') === 'true';
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Set the analytics opt-out status in localStorage.
|
|
113
|
+
*/
|
|
114
|
+
export function setOptOut(optOut: boolean): void {
|
|
115
|
+
try {
|
|
116
|
+
if (optOut) {
|
|
117
|
+
localStorage.setItem('agentuity_opt_out', 'true');
|
|
118
|
+
} else {
|
|
119
|
+
localStorage.removeItem('agentuity_opt_out');
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// localStorage not available
|
|
123
|
+
}
|
|
124
|
+
}
|