@agentuity/frontend 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics/beacon-standalone.d.ts +106 -0
- package/dist/analytics/beacon-standalone.d.ts.map +1 -0
- package/dist/analytics/beacon-standalone.js +577 -0
- package/dist/analytics/beacon-standalone.js.map +1 -0
- package/dist/analytics/index.d.ts +15 -5
- package/dist/analytics/index.d.ts.map +1 -1
- package/dist/analytics/index.js +21 -5
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/types.d.ts +63 -35
- package/dist/analytics/types.d.ts.map +1 -1
- package/dist/beacon-script.d.ts +16 -0
- package/dist/beacon-script.d.ts.map +1 -0
- package/dist/beacon-script.js +3 -0
- package/dist/beacon-script.js.map +1 -0
- package/dist/beacon.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/analytics/beacon-standalone.ts +718 -0
- package/src/analytics/index.ts +29 -8
- package/src/analytics/types.ts +78 -49
- package/src/beacon-script.ts +24 -0
- package/src/index.ts +10 -7
- package/dist/analytics/beacon.d.ts +0 -15
- package/dist/analytics/beacon.d.ts.map +0 -1
- package/dist/analytics/beacon.js +0 -177
- package/dist/analytics/beacon.js.map +0 -1
- package/dist/analytics/collectors/clicks.d.ts +0 -10
- package/dist/analytics/collectors/clicks.d.ts.map +0 -1
- package/dist/analytics/collectors/clicks.js +0 -84
- package/dist/analytics/collectors/clicks.js.map +0 -1
- package/dist/analytics/collectors/errors.d.ts +0 -5
- package/dist/analytics/collectors/errors.d.ts.map +0 -1
- package/dist/analytics/collectors/errors.js +0 -43
- package/dist/analytics/collectors/errors.js.map +0 -1
- package/dist/analytics/collectors/forms.d.ts +0 -5
- package/dist/analytics/collectors/forms.d.ts.map +0 -1
- package/dist/analytics/collectors/forms.js +0 -55
- package/dist/analytics/collectors/forms.js.map +0 -1
- package/dist/analytics/collectors/pageview.d.ts +0 -15
- package/dist/analytics/collectors/pageview.d.ts.map +0 -1
- package/dist/analytics/collectors/pageview.js +0 -64
- package/dist/analytics/collectors/pageview.js.map +0 -1
- package/dist/analytics/collectors/scroll.d.ts +0 -17
- package/dist/analytics/collectors/scroll.d.ts.map +0 -1
- package/dist/analytics/collectors/scroll.js +0 -93
- package/dist/analytics/collectors/scroll.js.map +0 -1
- package/dist/analytics/collectors/spa.d.ts +0 -10
- package/dist/analytics/collectors/spa.d.ts.map +0 -1
- package/dist/analytics/collectors/spa.js +0 -53
- package/dist/analytics/collectors/spa.js.map +0 -1
- package/dist/analytics/collectors/visibility.d.ts +0 -18
- package/dist/analytics/collectors/visibility.d.ts.map +0 -1
- package/dist/analytics/collectors/visibility.js +0 -81
- package/dist/analytics/collectors/visibility.js.map +0 -1
- package/dist/analytics/collectors/webvitals.d.ts +0 -6
- package/dist/analytics/collectors/webvitals.d.ts.map +0 -1
- package/dist/analytics/collectors/webvitals.js +0 -111
- package/dist/analytics/collectors/webvitals.js.map +0 -1
- package/dist/analytics/events.d.ts +0 -18
- package/dist/analytics/events.d.ts.map +0 -1
- package/dist/analytics/events.js +0 -126
- package/dist/analytics/events.js.map +0 -1
- package/dist/analytics/offline.d.ts +0 -19
- package/dist/analytics/offline.d.ts.map +0 -1
- package/dist/analytics/offline.js +0 -145
- package/dist/analytics/offline.js.map +0 -1
- package/src/analytics/beacon.ts +0 -203
- package/src/analytics/collectors/clicks.ts +0 -100
- package/src/analytics/collectors/errors.ts +0 -49
- package/src/analytics/collectors/forms.ts +0 -64
- package/src/analytics/collectors/pageview.ts +0 -76
- package/src/analytics/collectors/scroll.ts +0 -112
- package/src/analytics/collectors/spa.ts +0 -60
- package/src/analytics/collectors/visibility.ts +0 -94
- package/src/analytics/collectors/webvitals.ts +0 -129
- package/src/analytics/events.ts +0 -146
- package/src/analytics/offline.ts +0 -163
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone beacon script - this file is bundled and minified
|
|
3
|
+
* to create the production analytics.js served at /_agentuity/webanalytics/analytics.js
|
|
4
|
+
*
|
|
5
|
+
* This is the single source of truth for the beacon logic.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface ScrollEvent {
|
|
9
|
+
depth: number;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface AnalyticsCustomEvent {
|
|
14
|
+
timestamp: number;
|
|
15
|
+
name: string;
|
|
16
|
+
data: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface GeoLocation {
|
|
20
|
+
country?: string;
|
|
21
|
+
country_latitude?: string | number;
|
|
22
|
+
country_longitude?: string | number;
|
|
23
|
+
region?: string;
|
|
24
|
+
region_latitude?: string | number;
|
|
25
|
+
region_longitude?: string | number;
|
|
26
|
+
city?: string;
|
|
27
|
+
city_latitude?: string | number;
|
|
28
|
+
city_longitude?: string | number;
|
|
29
|
+
timezone?: string;
|
|
30
|
+
latitude?: string | number;
|
|
31
|
+
longitude?: string | number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface PageViewData {
|
|
35
|
+
id: string;
|
|
36
|
+
timestamp: number;
|
|
37
|
+
timezone_offset: number;
|
|
38
|
+
url: string;
|
|
39
|
+
path: string;
|
|
40
|
+
referrer: string;
|
|
41
|
+
title: string;
|
|
42
|
+
screen_width: number;
|
|
43
|
+
screen_height: number;
|
|
44
|
+
viewport_width: number;
|
|
45
|
+
viewport_height: number;
|
|
46
|
+
device_pixel_ratio: number;
|
|
47
|
+
user_agent: string;
|
|
48
|
+
language: string;
|
|
49
|
+
scroll_depth: number;
|
|
50
|
+
time_on_page: number;
|
|
51
|
+
scroll_events: ScrollEvent[];
|
|
52
|
+
custom_events: AnalyticsCustomEvent[];
|
|
53
|
+
load_time?: number;
|
|
54
|
+
dom_ready?: number;
|
|
55
|
+
ttfb?: number;
|
|
56
|
+
fcp?: number;
|
|
57
|
+
lcp?: number;
|
|
58
|
+
cls?: number;
|
|
59
|
+
inp?: number;
|
|
60
|
+
country?: string;
|
|
61
|
+
country_latitude?: number;
|
|
62
|
+
country_longitude?: number;
|
|
63
|
+
region?: string;
|
|
64
|
+
region_latitude?: number;
|
|
65
|
+
region_longitude?: number;
|
|
66
|
+
city?: string;
|
|
67
|
+
city_latitude?: number;
|
|
68
|
+
city_longitude?: number;
|
|
69
|
+
timezone?: string;
|
|
70
|
+
latitude?: number;
|
|
71
|
+
longitude?: number;
|
|
72
|
+
utm_source?: string;
|
|
73
|
+
utm_medium?: string;
|
|
74
|
+
utm_campaign?: string;
|
|
75
|
+
utm_term?: string;
|
|
76
|
+
utm_content?: string;
|
|
77
|
+
[key: string]: unknown;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface AnalyticsConfig {
|
|
81
|
+
enabled: boolean;
|
|
82
|
+
orgId: string;
|
|
83
|
+
projectId: string;
|
|
84
|
+
isDevmode: boolean;
|
|
85
|
+
trackClicks?: boolean;
|
|
86
|
+
trackScroll?: boolean;
|
|
87
|
+
trackWebVitals?: boolean;
|
|
88
|
+
trackErrors?: boolean;
|
|
89
|
+
trackSPANavigation?: boolean;
|
|
90
|
+
sampleRate?: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface SessionData {
|
|
94
|
+
threadId?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface AgentuityWindow {
|
|
98
|
+
__AGENTUITY_ANALYTICS__?: AnalyticsConfig;
|
|
99
|
+
__AGENTUITY_SESSION__?: SessionData;
|
|
100
|
+
agentuityAnalytics?: {
|
|
101
|
+
track: (name: string, properties?: Record<string, unknown>) => void;
|
|
102
|
+
identify: (userId: string, traits?: Record<string, unknown>) => void;
|
|
103
|
+
flush: () => void;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const COLLECT_ENDPOINT = '/_agentuity/webanalytics/collect';
|
|
108
|
+
const MAX_CUSTOM_EVENTS = 1000;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Safely stringify an object, handling circular references and other errors.
|
|
112
|
+
* Returns the JSON string on success, or a fallback string on failure.
|
|
113
|
+
*/
|
|
114
|
+
function safeStringify(obj: unknown): string {
|
|
115
|
+
if (obj === undefined || obj === null) {
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const seen = new WeakSet();
|
|
120
|
+
return JSON.stringify(obj, (_key, value) => {
|
|
121
|
+
if (typeof value === 'object' && value !== null) {
|
|
122
|
+
if (seen.has(value)) {
|
|
123
|
+
return '[Circular]';
|
|
124
|
+
}
|
|
125
|
+
seen.add(value);
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
});
|
|
129
|
+
} catch (err) {
|
|
130
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
131
|
+
console.warn('[Agentuity Analytics] Failed to stringify properties:', message);
|
|
132
|
+
return `[unserializable: ${message}]`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
(function () {
|
|
137
|
+
const w = window as Window & AgentuityWindow;
|
|
138
|
+
const d = document;
|
|
139
|
+
const configRaw = w.__AGENTUITY_ANALYTICS__;
|
|
140
|
+
|
|
141
|
+
if (!configRaw || !configRaw.enabled) return;
|
|
142
|
+
|
|
143
|
+
// Prevent duplicate initialization (e.g., from HMR)
|
|
144
|
+
const initFlag = w as unknown as { __AGENTUITY_BEACON_INIT__?: boolean };
|
|
145
|
+
if (configRaw.isDevmode) {
|
|
146
|
+
console.debug(
|
|
147
|
+
'[Agentuity Analytics] Script loaded, init flag:',
|
|
148
|
+
initFlag.__AGENTUITY_BEACON_INIT__,
|
|
149
|
+
'path:',
|
|
150
|
+
location.pathname
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (initFlag.__AGENTUITY_BEACON_INIT__) {
|
|
154
|
+
if (configRaw.isDevmode) {
|
|
155
|
+
console.debug('[Agentuity Analytics] Already initialized, skipping');
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
initFlag.__AGENTUITY_BEACON_INIT__ = true;
|
|
160
|
+
|
|
161
|
+
// Store in a non-nullable variable after the guard
|
|
162
|
+
const c: AnalyticsConfig = configRaw;
|
|
163
|
+
|
|
164
|
+
let geo: GeoLocation | null = null;
|
|
165
|
+
let sent = false;
|
|
166
|
+
let pageStart = Date.now();
|
|
167
|
+
let userId = '';
|
|
168
|
+
let userTraits: Record<string, string> = {};
|
|
169
|
+
|
|
170
|
+
const pv: PageViewData = {
|
|
171
|
+
id: '',
|
|
172
|
+
timestamp: 0,
|
|
173
|
+
timezone_offset: 0,
|
|
174
|
+
url: '',
|
|
175
|
+
path: '',
|
|
176
|
+
referrer: '',
|
|
177
|
+
title: '',
|
|
178
|
+
screen_width: 0,
|
|
179
|
+
screen_height: 0,
|
|
180
|
+
viewport_width: 0,
|
|
181
|
+
viewport_height: 0,
|
|
182
|
+
device_pixel_ratio: 1,
|
|
183
|
+
user_agent: '',
|
|
184
|
+
language: '',
|
|
185
|
+
scroll_depth: 0,
|
|
186
|
+
time_on_page: 0,
|
|
187
|
+
scroll_events: [],
|
|
188
|
+
custom_events: [],
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
function generateId(): string {
|
|
192
|
+
return crypto.randomUUID
|
|
193
|
+
? crypto.randomUUID()
|
|
194
|
+
: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getUTMParams(): Record<string, string> {
|
|
198
|
+
const params = new URLSearchParams(location.search);
|
|
199
|
+
const utm: Record<string, string> = {};
|
|
200
|
+
['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'].forEach((key) => {
|
|
201
|
+
const value = params.get(key);
|
|
202
|
+
if (value) utm[key] = value;
|
|
203
|
+
});
|
|
204
|
+
return utm;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Strip query string from URL to prevent sensitive data leakage
|
|
208
|
+
function stripQueryString(url: string): string {
|
|
209
|
+
if (!url) return '';
|
|
210
|
+
try {
|
|
211
|
+
const parsed = new URL(url);
|
|
212
|
+
return parsed.origin + parsed.pathname;
|
|
213
|
+
} catch {
|
|
214
|
+
// If URL parsing fails, try simple string split
|
|
215
|
+
return url.split('?')[0];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Full init - called on page load and SPA navigation
|
|
220
|
+
function init(): void {
|
|
221
|
+
pv.id = generateId();
|
|
222
|
+
pv.timestamp = Date.now();
|
|
223
|
+
pv.timezone_offset = new Date().getTimezoneOffset();
|
|
224
|
+
pv.url = stripQueryString(location.href);
|
|
225
|
+
pv.path = location.pathname;
|
|
226
|
+
pv.referrer = stripQueryString(d.referrer);
|
|
227
|
+
pv.title = d.title || '';
|
|
228
|
+
pv.screen_width = screen.width || 0;
|
|
229
|
+
pv.screen_height = screen.height || 0;
|
|
230
|
+
pv.viewport_width = innerWidth || 0;
|
|
231
|
+
pv.viewport_height = innerHeight || 0;
|
|
232
|
+
pv.device_pixel_ratio = devicePixelRatio || 1;
|
|
233
|
+
pv.user_agent = navigator.userAgent || '';
|
|
234
|
+
pv.language = navigator.language || '';
|
|
235
|
+
|
|
236
|
+
const utm = getUTMParams();
|
|
237
|
+
for (const k in utm) {
|
|
238
|
+
pv[k] = utm[k];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
pv.scroll_events = [];
|
|
242
|
+
pv.custom_events = [];
|
|
243
|
+
pv.scroll_depth = 0;
|
|
244
|
+
pv.fcp = 0;
|
|
245
|
+
pv.lcp = 0;
|
|
246
|
+
pv.cls = 0;
|
|
247
|
+
pv.inp = 0;
|
|
248
|
+
sent = false;
|
|
249
|
+
pageStart = Date.now();
|
|
250
|
+
|
|
251
|
+
if (typeof performance !== 'undefined' && performance.getEntriesByType) {
|
|
252
|
+
const nav = performance.getEntriesByType('navigation')[0] as
|
|
253
|
+
| PerformanceNavigationTiming
|
|
254
|
+
| undefined;
|
|
255
|
+
if (nav) {
|
|
256
|
+
pv.load_time = Math.round(nav.loadEventEnd - nav.startTime);
|
|
257
|
+
pv.dom_ready = Math.round(nav.domContentLoadedEventEnd - nav.startTime);
|
|
258
|
+
pv.ttfb = Math.round(nav.responseStart - nav.requestStart);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (c.isDevmode) {
|
|
263
|
+
console.debug('[Agentuity Analytics] Session started (full init):', pv.id);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Soft reset - called when user returns to page (keeps page-level metrics)
|
|
268
|
+
function resetSession(): void {
|
|
269
|
+
pv.id = generateId();
|
|
270
|
+
pv.timestamp = Date.now();
|
|
271
|
+
pv.scroll_events = [];
|
|
272
|
+
pv.custom_events = [];
|
|
273
|
+
pv.scroll_depth = 0;
|
|
274
|
+
pv.time_on_page = 0;
|
|
275
|
+
sent = false;
|
|
276
|
+
pageStart = Date.now();
|
|
277
|
+
|
|
278
|
+
if (c.isDevmode) {
|
|
279
|
+
console.debug('[Agentuity Analytics] Session started (soft reset):', pv.id);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Fetch geo data
|
|
284
|
+
fetch('https://agentuity.sh/location')
|
|
285
|
+
.then((r) => r.json())
|
|
286
|
+
.then((g: GeoLocation) => {
|
|
287
|
+
geo = g;
|
|
288
|
+
try {
|
|
289
|
+
sessionStorage.setItem('agentuity_geo', JSON.stringify(g));
|
|
290
|
+
} catch {
|
|
291
|
+
// Ignore
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
.catch(() => {
|
|
295
|
+
try {
|
|
296
|
+
const cached = sessionStorage.getItem('agentuity_geo');
|
|
297
|
+
if (cached) geo = JSON.parse(cached);
|
|
298
|
+
} catch {
|
|
299
|
+
// Ignore
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Try to load cached geo immediately
|
|
304
|
+
try {
|
|
305
|
+
const cached = sessionStorage.getItem('agentuity_geo');
|
|
306
|
+
if (cached) geo = JSON.parse(cached);
|
|
307
|
+
} catch {
|
|
308
|
+
// Ignore
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function getSession(): SessionData | undefined {
|
|
312
|
+
return w.__AGENTUITY_SESSION__;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function send(force = false): void {
|
|
316
|
+
if (sent && !force) {
|
|
317
|
+
if (c.isDevmode) {
|
|
318
|
+
console.debug('[Agentuity Analytics] send() skipped - already sent');
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (c.sampleRate !== undefined && c.sampleRate < 1 && Math.random() > c.sampleRate) return;
|
|
323
|
+
|
|
324
|
+
sent = true;
|
|
325
|
+
pv.time_on_page = Date.now() - pageStart;
|
|
326
|
+
|
|
327
|
+
if (geo) {
|
|
328
|
+
pv.country = geo.country || '';
|
|
329
|
+
if (geo.country_latitude) pv.country_latitude = parseFloat(String(geo.country_latitude));
|
|
330
|
+
if (geo.country_longitude)
|
|
331
|
+
pv.country_longitude = parseFloat(String(geo.country_longitude));
|
|
332
|
+
pv.region = geo.region || '';
|
|
333
|
+
if (geo.region_latitude) pv.region_latitude = parseFloat(String(geo.region_latitude));
|
|
334
|
+
if (geo.region_longitude) pv.region_longitude = parseFloat(String(geo.region_longitude));
|
|
335
|
+
pv.city = geo.city || '';
|
|
336
|
+
if (geo.city_latitude) pv.city_latitude = parseFloat(String(geo.city_latitude));
|
|
337
|
+
if (geo.city_longitude) pv.city_longitude = parseFloat(String(geo.city_longitude));
|
|
338
|
+
pv.timezone = geo.timezone || '';
|
|
339
|
+
if (geo.latitude) pv.latitude = parseFloat(String(geo.latitude));
|
|
340
|
+
if (geo.longitude) pv.longitude = parseFloat(String(geo.longitude));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (pv.cls) {
|
|
344
|
+
pv.cls = Math.round(pv.cls * 1000) / 1000;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const s = getSession();
|
|
348
|
+
const vid = localStorage.getItem('agentuity_visitor_id') || 'vid_' + generateId();
|
|
349
|
+
try {
|
|
350
|
+
localStorage.setItem('agentuity_visitor_id', vid);
|
|
351
|
+
} catch {
|
|
352
|
+
// Ignore
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const payload = {
|
|
356
|
+
org_id: c.orgId,
|
|
357
|
+
project_id: c.projectId,
|
|
358
|
+
thread_id: s?.threadId || '',
|
|
359
|
+
visitor_id: vid,
|
|
360
|
+
user_id: userId,
|
|
361
|
+
user_traits: userTraits,
|
|
362
|
+
is_devmode: c.isDevmode,
|
|
363
|
+
pageview: pv,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// Clear pending data since we're sending now
|
|
367
|
+
try {
|
|
368
|
+
sessionStorage.removeItem('agentuity_pending_pageview');
|
|
369
|
+
} catch {
|
|
370
|
+
// Storage may be unavailable
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (c.isDevmode) {
|
|
374
|
+
console.debug('[Agentuity Analytics]', JSON.stringify(payload, null, 2));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const body = JSON.stringify(payload);
|
|
379
|
+
if (navigator.sendBeacon) {
|
|
380
|
+
navigator.sendBeacon(COLLECT_ENDPOINT, body);
|
|
381
|
+
} else {
|
|
382
|
+
fetch(COLLECT_ENDPOINT, {
|
|
383
|
+
method: 'POST',
|
|
384
|
+
body,
|
|
385
|
+
keepalive: true,
|
|
386
|
+
}).catch(() => {
|
|
387
|
+
// Silent failure
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Send on page hide, reset session on page visible
|
|
393
|
+
d.addEventListener('visibilitychange', () => {
|
|
394
|
+
if (c.isDevmode) {
|
|
395
|
+
console.debug('[Agentuity Analytics] visibilitychange:', d.visibilityState, 'sent:', sent);
|
|
396
|
+
}
|
|
397
|
+
if (d.visibilityState === 'hidden') {
|
|
398
|
+
send();
|
|
399
|
+
} else if (d.visibilityState === 'visible') {
|
|
400
|
+
// User returned to the page - start a new attention session
|
|
401
|
+
// Keep page-level metrics (url, geo, vitals) but reset session-level metrics
|
|
402
|
+
resetSession();
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
w.addEventListener('pagehide', () => {
|
|
406
|
+
if (c.isDevmode) {
|
|
407
|
+
console.debug('[Agentuity Analytics] pagehide event');
|
|
408
|
+
try {
|
|
409
|
+
sessionStorage.setItem('agentuity_last_event', `pagehide:${Date.now()}:${pv.path}`);
|
|
410
|
+
} catch {
|
|
411
|
+
// Storage may be unavailable
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
send();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Catch hard navigations (URL change, link click to new page, refresh)
|
|
418
|
+
w.addEventListener('beforeunload', () => {
|
|
419
|
+
if (c.isDevmode) {
|
|
420
|
+
console.debug('[Agentuity Analytics] beforeunload event');
|
|
421
|
+
try {
|
|
422
|
+
sessionStorage.setItem('agentuity_last_event', `beforeunload:${Date.now()}:${pv.path}`);
|
|
423
|
+
} catch {
|
|
424
|
+
// Storage may be unavailable
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
send();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// In devmode, check if previous page sent data (helps verify unload events work)
|
|
431
|
+
if (c.isDevmode) {
|
|
432
|
+
try {
|
|
433
|
+
const lastEvent = sessionStorage.getItem('agentuity_last_event');
|
|
434
|
+
if (lastEvent) {
|
|
435
|
+
console.debug('[Agentuity Analytics] Previous page event:', lastEvent);
|
|
436
|
+
sessionStorage.removeItem('agentuity_last_event');
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
// Storage may be unavailable
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Fallback: check for unsent data from previous page (in case unload events didn't fire)
|
|
444
|
+
try {
|
|
445
|
+
const pendingData = sessionStorage.getItem('agentuity_pending_pageview');
|
|
446
|
+
if (pendingData) {
|
|
447
|
+
sessionStorage.removeItem('agentuity_pending_pageview');
|
|
448
|
+
const pending = JSON.parse(pendingData);
|
|
449
|
+
// Only send if it's from a different page
|
|
450
|
+
if (pending.pageview?.path !== location.pathname) {
|
|
451
|
+
if (c.isDevmode) {
|
|
452
|
+
console.debug(
|
|
453
|
+
'[Agentuity Analytics] Sending unsent data from previous page:',
|
|
454
|
+
pending.pageview?.path
|
|
455
|
+
);
|
|
456
|
+
console.debug('[Agentuity Analytics]', JSON.stringify(pending, null, 2));
|
|
457
|
+
} else {
|
|
458
|
+
const body = JSON.stringify(pending);
|
|
459
|
+
if (navigator.sendBeacon) {
|
|
460
|
+
navigator.sendBeacon(COLLECT_ENDPOINT, body);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
// Storage or JSON parsing may fail
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Store current pageview data periodically so it can be recovered if unload events don't fire
|
|
470
|
+
function savePendingData(): void {
|
|
471
|
+
try {
|
|
472
|
+
pv.time_on_page = Date.now() - pageStart;
|
|
473
|
+
const s = getSession();
|
|
474
|
+
const vid = localStorage.getItem('agentuity_visitor_id') || 'vid_' + generateId();
|
|
475
|
+
const payload = {
|
|
476
|
+
org_id: c.orgId,
|
|
477
|
+
project_id: c.projectId,
|
|
478
|
+
thread_id: s?.threadId || '',
|
|
479
|
+
visitor_id: vid,
|
|
480
|
+
user_id: userId,
|
|
481
|
+
user_traits: userTraits,
|
|
482
|
+
is_devmode: c.isDevmode,
|
|
483
|
+
pageview: { ...pv },
|
|
484
|
+
};
|
|
485
|
+
sessionStorage.setItem('agentuity_pending_pageview', JSON.stringify(payload));
|
|
486
|
+
} catch {
|
|
487
|
+
// Storage may be unavailable
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Save pending data every 2 seconds
|
|
492
|
+
setInterval(savePendingData, 2000);
|
|
493
|
+
// Also save on any interaction
|
|
494
|
+
d.addEventListener('click', savePendingData, { passive: true });
|
|
495
|
+
d.addEventListener('scroll', savePendingData, { passive: true, once: true });
|
|
496
|
+
|
|
497
|
+
if (c.isDevmode) {
|
|
498
|
+
console.debug('[Agentuity Analytics] Beacon initialized, visibility:', d.visibilityState);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Scroll tracking
|
|
502
|
+
if (c.trackScroll !== false) {
|
|
503
|
+
const scrolled = new Set<number>();
|
|
504
|
+
|
|
505
|
+
function getScrollDepth(): number {
|
|
506
|
+
const st = w.scrollY || d.documentElement.scrollTop;
|
|
507
|
+
const sh = d.documentElement.scrollHeight - d.documentElement.clientHeight;
|
|
508
|
+
return sh <= 0 ? 100 : Math.min(100, Math.round((st / sh) * 100));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
w.addEventListener(
|
|
512
|
+
'scroll',
|
|
513
|
+
() => {
|
|
514
|
+
const dp = getScrollDepth();
|
|
515
|
+
if (dp > pv.scroll_depth) pv.scroll_depth = dp;
|
|
516
|
+
|
|
517
|
+
[25, 50, 75, 100].forEach((m) => {
|
|
518
|
+
if (dp >= m && !scrolled.has(m)) {
|
|
519
|
+
scrolled.add(m);
|
|
520
|
+
pv.scroll_events.push({
|
|
521
|
+
depth: m,
|
|
522
|
+
timestamp: Date.now() - pageStart,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
},
|
|
527
|
+
{ passive: true }
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Web Vitals tracking
|
|
532
|
+
if (c.trackWebVitals !== false && typeof PerformanceObserver !== 'undefined') {
|
|
533
|
+
// FCP
|
|
534
|
+
try {
|
|
535
|
+
const fcpObs = new PerformanceObserver((list) => {
|
|
536
|
+
list.getEntries().forEach((entry) => {
|
|
537
|
+
if (entry.name === 'first-contentful-paint') {
|
|
538
|
+
pv.fcp = Math.round(entry.startTime);
|
|
539
|
+
fcpObs.disconnect();
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
fcpObs.observe({ type: 'paint', buffered: true });
|
|
544
|
+
} catch {
|
|
545
|
+
// Not supported
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// LCP
|
|
549
|
+
try {
|
|
550
|
+
new PerformanceObserver((list) => {
|
|
551
|
+
const entries = list.getEntries();
|
|
552
|
+
if (entries.length) {
|
|
553
|
+
pv.lcp = Math.round(entries[entries.length - 1].startTime);
|
|
554
|
+
}
|
|
555
|
+
}).observe({ type: 'largest-contentful-paint', buffered: true });
|
|
556
|
+
} catch {
|
|
557
|
+
// Not supported
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// CLS
|
|
561
|
+
try {
|
|
562
|
+
new PerformanceObserver((list) => {
|
|
563
|
+
list.getEntries().forEach((entry) => {
|
|
564
|
+
const layoutShift = entry as PerformanceEntry & {
|
|
565
|
+
hadRecentInput?: boolean;
|
|
566
|
+
value?: number;
|
|
567
|
+
};
|
|
568
|
+
if (!layoutShift.hadRecentInput && layoutShift.value) {
|
|
569
|
+
pv.cls = (pv.cls || 0) + layoutShift.value;
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
}).observe({ type: 'layout-shift', buffered: true });
|
|
573
|
+
} catch {
|
|
574
|
+
// Not supported
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// INP
|
|
578
|
+
try {
|
|
579
|
+
new PerformanceObserver((list) => {
|
|
580
|
+
list.getEntries().forEach((entry) => {
|
|
581
|
+
const eventEntry = entry as PerformanceEntry & { duration?: number };
|
|
582
|
+
if (eventEntry.duration && eventEntry.duration > (pv.inp || 0)) {
|
|
583
|
+
pv.inp = Math.round(eventEntry.duration);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}).observe({ type: 'event', buffered: true });
|
|
587
|
+
} catch {
|
|
588
|
+
// Not supported
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// SPA navigation tracking
|
|
593
|
+
if (c.trackSPANavigation !== false) {
|
|
594
|
+
const origPush = history.pushState;
|
|
595
|
+
const origReplace = history.replaceState;
|
|
596
|
+
let currentPath = location.pathname + location.search;
|
|
597
|
+
let lastHref = location.href;
|
|
598
|
+
|
|
599
|
+
if (c.isDevmode) {
|
|
600
|
+
console.debug('[Agentuity Analytics] SPA tracking enabled, initial path:', currentPath);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function handleNav(): void {
|
|
604
|
+
const newPath = location.pathname + location.search;
|
|
605
|
+
if (newPath !== currentPath) {
|
|
606
|
+
if (c.isDevmode) {
|
|
607
|
+
console.debug('[Agentuity Analytics] SPA navigation:', currentPath, '->', newPath);
|
|
608
|
+
}
|
|
609
|
+
send(true); // Force send on SPA navigation
|
|
610
|
+
currentPath = newPath;
|
|
611
|
+
lastHref = location.href;
|
|
612
|
+
init();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
history.pushState = function (...args) {
|
|
617
|
+
origPush.apply(this, args);
|
|
618
|
+
setTimeout(handleNav, 0);
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
history.replaceState = function (...args) {
|
|
622
|
+
origReplace.apply(this, args);
|
|
623
|
+
setTimeout(handleNav, 0);
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
w.addEventListener('popstate', handleNav);
|
|
627
|
+
|
|
628
|
+
// Fallback: poll for URL changes in case router bypasses history API
|
|
629
|
+
setInterval(() => {
|
|
630
|
+
if (location.href !== lastHref) {
|
|
631
|
+
lastHref = location.href;
|
|
632
|
+
handleNav();
|
|
633
|
+
}
|
|
634
|
+
}, 200);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Click tracking
|
|
638
|
+
if (c.trackClicks !== false) {
|
|
639
|
+
d.addEventListener(
|
|
640
|
+
'click',
|
|
641
|
+
(e) => {
|
|
642
|
+
const target = e.target as Element | null;
|
|
643
|
+
if (!target) return;
|
|
644
|
+
|
|
645
|
+
const analyticsEl = target.closest('[data-analytics]');
|
|
646
|
+
if (!analyticsEl) return;
|
|
647
|
+
|
|
648
|
+
if (pv.custom_events.length < MAX_CUSTOM_EVENTS) {
|
|
649
|
+
pv.custom_events.push({
|
|
650
|
+
timestamp: Date.now(),
|
|
651
|
+
name: 'click:' + analyticsEl.getAttribute('data-analytics'),
|
|
652
|
+
data: '',
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
true
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Error tracking
|
|
661
|
+
if (c.trackErrors !== false) {
|
|
662
|
+
w.addEventListener('error', (e) => {
|
|
663
|
+
if (pv.custom_events.length < MAX_CUSTOM_EVENTS) {
|
|
664
|
+
pv.custom_events.push({
|
|
665
|
+
timestamp: Date.now(),
|
|
666
|
+
name: 'error:js_error',
|
|
667
|
+
data: JSON.stringify({
|
|
668
|
+
message: e.message || 'Unknown',
|
|
669
|
+
filename: e.filename || '',
|
|
670
|
+
lineno: e.lineno || 0,
|
|
671
|
+
}),
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
w.addEventListener('unhandledrejection', (e) => {
|
|
677
|
+
if (pv.custom_events.length < MAX_CUSTOM_EVENTS) {
|
|
678
|
+
pv.custom_events.push({
|
|
679
|
+
timestamp: Date.now(),
|
|
680
|
+
name: 'error:unhandled_rejection',
|
|
681
|
+
data: JSON.stringify({
|
|
682
|
+
message: e.reason instanceof Error ? e.reason.message : String(e.reason),
|
|
683
|
+
}),
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Initialize on load
|
|
690
|
+
if (d.readyState === 'complete') {
|
|
691
|
+
init();
|
|
692
|
+
} else {
|
|
693
|
+
w.addEventListener('load', init);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Public API
|
|
697
|
+
w.agentuityAnalytics = {
|
|
698
|
+
track(name: string, properties?: Record<string, unknown>): void {
|
|
699
|
+
if (pv.custom_events.length < MAX_CUSTOM_EVENTS) {
|
|
700
|
+
pv.custom_events.push({
|
|
701
|
+
timestamp: Date.now(),
|
|
702
|
+
name,
|
|
703
|
+
data: safeStringify(properties),
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
identify(id: string, traits?: Record<string, unknown>): void {
|
|
708
|
+
userId = id;
|
|
709
|
+
if (traits) {
|
|
710
|
+
userTraits = {};
|
|
711
|
+
for (const [key, value] of Object.entries(traits)) {
|
|
712
|
+
userTraits[key] = String(value);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
flush: () => send(true),
|
|
717
|
+
};
|
|
718
|
+
})();
|