@arthurreira/analytics 0.20.0 → 0.22.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/dist/client.d.ts +11 -2
- package/dist/client.js +143 -36
- package/dist/index.d.ts +10 -2
- package/dist/index.js +115 -29
- package/package.json +9 -5
package/dist/client.d.ts
CHANGED
|
@@ -2,16 +2,20 @@ interface AnalyticsProps {
|
|
|
2
2
|
apiKey: string;
|
|
3
3
|
apiUrl: string;
|
|
4
4
|
wsUrl?: string;
|
|
5
|
+
routeName?: string;
|
|
5
6
|
}
|
|
6
|
-
declare function Analytics({ apiKey, apiUrl, wsUrl }: AnalyticsProps): null;
|
|
7
|
+
declare function Analytics({ apiKey, apiUrl, wsUrl, routeName }: AnalyticsProps): null;
|
|
7
8
|
|
|
8
9
|
interface ExceptionFields {
|
|
9
10
|
exception_type: string;
|
|
10
11
|
exception_message: string;
|
|
11
12
|
stack_trace: string | null;
|
|
13
|
+
exception_filename?: string | null;
|
|
14
|
+
exception_lineno?: number | null;
|
|
15
|
+
exception_colno?: number | null;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
declare function useAnalytics(apiUrl: string, apiKey: string, currentPath: string, wsUrl?: string): {
|
|
18
|
+
declare function useAnalytics(apiUrl: string, apiKey: string, currentPath: string, wsUrl?: string, routeName?: string): {
|
|
15
19
|
trackPageview: (path: string) => void;
|
|
16
20
|
trackClick: (e: MouseEvent, element: HTMLElement) => void;
|
|
17
21
|
trackScroll: (depth: number) => void;
|
|
@@ -20,6 +24,11 @@ declare function useAnalytics(apiUrl: string, apiKey: string, currentPath: strin
|
|
|
20
24
|
trackError: (error: Error) => void;
|
|
21
25
|
trackCTA: (ctaId: string, ctaVariant?: string) => void;
|
|
22
26
|
trackSearch: (query: string) => void;
|
|
27
|
+
trackWebVital: (metric: {
|
|
28
|
+
name: string;
|
|
29
|
+
value: number;
|
|
30
|
+
rating: string;
|
|
31
|
+
}) => void;
|
|
23
32
|
};
|
|
24
33
|
|
|
25
34
|
export { Analytics, useAnalytics };
|
package/dist/client.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// src/components/Analytics.tsx
|
|
5
5
|
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
6
6
|
import { usePathname } from "next/navigation";
|
|
7
|
+
import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals";
|
|
7
8
|
|
|
8
9
|
// src/hooks/useAnalytics.ts
|
|
9
10
|
import { useEffect, useRef } from "react";
|
|
@@ -17,12 +18,60 @@ function isOptedOut() {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
// src/lib/api.ts
|
|
20
|
-
var BASE_FIELDS = (sessionId, eventType, path) => ({
|
|
21
|
+
var BASE_FIELDS = (sessionId, eventType, path, eventName, eventCategory) => ({
|
|
21
22
|
session_id: sessionId,
|
|
22
23
|
event_type: eventType,
|
|
24
|
+
event_name: eventName ?? null,
|
|
25
|
+
event_category: eventCategory ?? null,
|
|
23
26
|
path,
|
|
24
|
-
page_url: typeof window !== "undefined" ? `${window.location.origin}${path}` : ""
|
|
27
|
+
page_url: typeof window !== "undefined" ? `${window.location.origin}${path}` : "",
|
|
28
|
+
// Page context
|
|
29
|
+
page_title: typeof document !== "undefined" ? document.title || null : null,
|
|
30
|
+
referrer: typeof document !== "undefined" ? document.referrer || null : null,
|
|
31
|
+
// Viewport
|
|
32
|
+
viewport_width: typeof window !== "undefined" ? window.innerWidth : null,
|
|
33
|
+
viewport_height: typeof window !== "undefined" ? window.innerHeight : null,
|
|
34
|
+
// Timing — how long since page loaded
|
|
35
|
+
time_on_page_ms: typeof performance !== "undefined" ? Math.round(performance.now()) : null,
|
|
36
|
+
// User engagement state at moment of event
|
|
37
|
+
document_visibility: typeof document !== "undefined" ? document.visibilityState : null
|
|
25
38
|
});
|
|
39
|
+
function getConnectionType() {
|
|
40
|
+
if (typeof navigator === "undefined") return null;
|
|
41
|
+
const nav = navigator;
|
|
42
|
+
return nav.connection?.effectiveType ?? null;
|
|
43
|
+
}
|
|
44
|
+
function resolveInteractiveTarget(el) {
|
|
45
|
+
const interactive = el.closest(
|
|
46
|
+
'a, button, [role="button"], input, select, textarea, label, summary'
|
|
47
|
+
);
|
|
48
|
+
return interactive ?? el;
|
|
49
|
+
}
|
|
50
|
+
function resolveElementHref(element) {
|
|
51
|
+
if (element.tagName.toLowerCase() === "a") {
|
|
52
|
+
return element.href || null;
|
|
53
|
+
}
|
|
54
|
+
const anchor = element.closest("a");
|
|
55
|
+
return anchor ? anchor.href || null : null;
|
|
56
|
+
}
|
|
57
|
+
function safeClassName(el) {
|
|
58
|
+
const cn = el.className;
|
|
59
|
+
return typeof cn === "string" ? cn || null : null;
|
|
60
|
+
}
|
|
61
|
+
function cleanLabel(raw, maxLen) {
|
|
62
|
+
if (!raw) return null;
|
|
63
|
+
const cleaned = raw.replace(/\s+/g, " ").trim().slice(0, maxLen);
|
|
64
|
+
return cleaned || null;
|
|
65
|
+
}
|
|
66
|
+
function isExternalHref(href) {
|
|
67
|
+
if (!href) return null;
|
|
68
|
+
if (typeof window === "undefined") return null;
|
|
69
|
+
try {
|
|
70
|
+
return new URL(href).origin !== window.location.origin;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
26
75
|
function getGpu() {
|
|
27
76
|
try {
|
|
28
77
|
const canvas = document.createElement("canvas");
|
|
@@ -61,7 +110,6 @@ async function createSession(apiUrl, apiKey, visitorId) {
|
|
|
61
110
|
sessionData.utm_term = params.get("utm_term");
|
|
62
111
|
sessionData.utm_content = params.get("utm_content");
|
|
63
112
|
}
|
|
64
|
-
console.log(`[AF Analytics SDK] Creating session at ${apiUrl}/sessions with API key: ${apiKey.slice(0, 10)}...`);
|
|
65
113
|
try {
|
|
66
114
|
const response = await fetch(`${apiUrl}/sessions`, {
|
|
67
115
|
method: "POST",
|
|
@@ -71,17 +119,10 @@ async function createSession(apiUrl, apiKey, visitorId) {
|
|
|
71
119
|
},
|
|
72
120
|
body: JSON.stringify(sessionData)
|
|
73
121
|
});
|
|
74
|
-
|
|
75
|
-
if (!response.ok) {
|
|
76
|
-
const errText = await response.text();
|
|
77
|
-
console.error(`[AF Analytics SDK] Session creation failed: ${errText}`);
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
122
|
+
if (!response.ok) return null;
|
|
80
123
|
const data = await response.json();
|
|
81
|
-
console.log(`[AF Analytics SDK] Session created: ${data.id}`);
|
|
82
124
|
return data.id ?? null;
|
|
83
|
-
} catch
|
|
84
|
-
console.error(`[AF Analytics SDK] Session creation error: ${err}`);
|
|
125
|
+
} catch {
|
|
85
126
|
return null;
|
|
86
127
|
}
|
|
87
128
|
}
|
|
@@ -96,45 +137,73 @@ async function sendEvent(apiUrl, apiKey, payload) {
|
|
|
96
137
|
body: JSON.stringify(payload)
|
|
97
138
|
});
|
|
98
139
|
}
|
|
99
|
-
async function trackPageview(apiUrl, apiKey, sessionId, path) {
|
|
140
|
+
async function trackPageview(apiUrl, apiKey, sessionId, path, routeName) {
|
|
141
|
+
const navigationEntry = typeof performance !== "undefined" ? performance.getEntriesByType("navigation")[0] : void 0;
|
|
142
|
+
const title = typeof document !== "undefined" ? document.title || null : null;
|
|
143
|
+
const eventName = title || path;
|
|
100
144
|
await sendEvent(apiUrl, apiKey, {
|
|
101
|
-
...BASE_FIELDS(sessionId, "pageview", path),
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
145
|
+
...BASE_FIELDS(sessionId, "pageview", path, eventName, "navigation"),
|
|
146
|
+
scroll_depth: 0,
|
|
147
|
+
route_name: routeName ?? null,
|
|
148
|
+
navigation_type: navigationEntry?.type ?? null,
|
|
149
|
+
connection_type: getConnectionType()
|
|
105
150
|
});
|
|
106
151
|
}
|
|
107
152
|
async function trackClick(apiUrl, apiKey, sessionId, path, e, element) {
|
|
153
|
+
const target = resolveInteractiveTarget(element);
|
|
154
|
+
const href = resolveElementHref(target);
|
|
155
|
+
const ariaLabel = target.getAttribute("aria-label");
|
|
156
|
+
const rawText = target.innerText ?? target.textContent ?? void 0;
|
|
157
|
+
const eventName = cleanLabel(ariaLabel, 60) ?? cleanLabel(rawText, 60) ?? target.tagName.toLowerCase();
|
|
108
158
|
await sendEvent(apiUrl, apiKey, {
|
|
109
|
-
...BASE_FIELDS(sessionId, "click", path),
|
|
159
|
+
...BASE_FIELDS(sessionId, "click", path, eventName, "interaction"),
|
|
110
160
|
x_position: e.clientX,
|
|
111
161
|
y_position: e.clientY,
|
|
112
|
-
element_id:
|
|
113
|
-
element_class:
|
|
114
|
-
element_text:
|
|
162
|
+
element_id: target.id || null,
|
|
163
|
+
element_class: safeClassName(target),
|
|
164
|
+
element_text: cleanLabel(rawText, 100),
|
|
165
|
+
tag_name: target.tagName.toLowerCase(),
|
|
166
|
+
element_href: href,
|
|
167
|
+
element_role: target.getAttribute("role") || null,
|
|
168
|
+
element_aria_label: ariaLabel || null,
|
|
169
|
+
element_type: target.type || null,
|
|
170
|
+
element_name: target.name || null,
|
|
171
|
+
is_external_link: isExternalHref(href),
|
|
172
|
+
modifier_keys: {
|
|
173
|
+
alt: e.altKey,
|
|
174
|
+
ctrl: e.ctrlKey,
|
|
175
|
+
meta: e.metaKey,
|
|
176
|
+
shift: e.shiftKey
|
|
177
|
+
},
|
|
178
|
+
click_count: e.detail
|
|
115
179
|
});
|
|
116
180
|
}
|
|
117
181
|
async function trackScroll(apiUrl, apiKey, sessionId, path, depth) {
|
|
118
182
|
await sendEvent(apiUrl, apiKey, {
|
|
119
|
-
...BASE_FIELDS(sessionId, "scroll", path),
|
|
120
|
-
scroll_depth: depth
|
|
183
|
+
...BASE_FIELDS(sessionId, "scroll", path, `Scroll ${depth}%`, "interaction"),
|
|
184
|
+
scroll_depth: depth,
|
|
185
|
+
scroll_y_px: typeof window !== "undefined" ? window.scrollY : null,
|
|
186
|
+
document_height_px: typeof document !== "undefined" ? document.documentElement.scrollHeight : null
|
|
121
187
|
});
|
|
122
188
|
}
|
|
123
189
|
async function trackCopy(apiUrl, apiKey, sessionId, path) {
|
|
190
|
+
const selection = typeof window !== "undefined" ? window.getSelection()?.toString() ?? null : null;
|
|
124
191
|
await sendEvent(apiUrl, apiKey, {
|
|
125
|
-
...BASE_FIELDS(sessionId, "copy", path),
|
|
126
|
-
copied_text:
|
|
192
|
+
...BASE_FIELDS(sessionId, "copy", path, "Copy", "interaction"),
|
|
193
|
+
copied_text: selection?.slice(0, 200) || null,
|
|
194
|
+
selection_length: selection?.length ?? 0
|
|
127
195
|
});
|
|
128
196
|
}
|
|
129
197
|
async function trackSearch(apiUrl, apiKey, sessionId, path, query) {
|
|
130
198
|
await sendEvent(apiUrl, apiKey, {
|
|
131
|
-
...BASE_FIELDS(sessionId, "search", path),
|
|
199
|
+
...BASE_FIELDS(sessionId, "search", path, `Search: ${query.slice(0, 60)}`, "interaction"),
|
|
132
200
|
search_query: query
|
|
133
201
|
});
|
|
134
202
|
}
|
|
135
203
|
async function trackException(apiUrl, apiKey, sessionId, path, fields) {
|
|
204
|
+
const eventName = fields.exception_type ? `${fields.exception_type}: ${fields.exception_message.slice(0, 60)}` : fields.exception_message.slice(0, 80);
|
|
136
205
|
await sendEvent(apiUrl, apiKey, {
|
|
137
|
-
...BASE_FIELDS(sessionId, "js_exception", path),
|
|
206
|
+
...BASE_FIELDS(sessionId, "js_exception", path, eventName, "error"),
|
|
138
207
|
...fields
|
|
139
208
|
});
|
|
140
209
|
}
|
|
@@ -146,12 +215,29 @@ async function trackError(apiUrl, apiKey, sessionId, path, error) {
|
|
|
146
215
|
});
|
|
147
216
|
}
|
|
148
217
|
async function trackCTA(apiUrl, apiKey, sessionId, path, ctaId, ctaVariant) {
|
|
218
|
+
const eventName = ctaVariant ? `${ctaId} (${ctaVariant})` : ctaId;
|
|
149
219
|
await sendEvent(apiUrl, apiKey, {
|
|
150
|
-
...BASE_FIELDS(sessionId, "cta_click", path),
|
|
220
|
+
...BASE_FIELDS(sessionId, "cta_click", path, eventName, "conversion"),
|
|
151
221
|
cta_id: ctaId,
|
|
152
222
|
cta_variant: ctaVariant || null
|
|
153
223
|
});
|
|
154
224
|
}
|
|
225
|
+
var WEB_VITAL_FIELDS = {
|
|
226
|
+
lcp_ms: "lcp_ms",
|
|
227
|
+
cls_score: "cls_score",
|
|
228
|
+
inp_ms: "inp_ms",
|
|
229
|
+
ttfb_ms: "ttfb_ms",
|
|
230
|
+
fcp_ms: "fcp_ms"
|
|
231
|
+
};
|
|
232
|
+
async function trackWebVital(apiUrl, apiKey, sessionId, path, metric) {
|
|
233
|
+
const field = WEB_VITAL_FIELDS[metric.name];
|
|
234
|
+
if (!field) return;
|
|
235
|
+
await sendEvent(apiUrl, apiKey, {
|
|
236
|
+
...BASE_FIELDS(sessionId, "web_vital", path, metric.name.toUpperCase(), "performance"),
|
|
237
|
+
[field]: metric.value,
|
|
238
|
+
rating: metric.rating
|
|
239
|
+
});
|
|
240
|
+
}
|
|
155
241
|
|
|
156
242
|
// src/lib/presence.ts
|
|
157
243
|
function connectPresence(apiKey, sessionId, wsUrl) {
|
|
@@ -306,7 +392,7 @@ async function getOrCreateSession(apiUrl, apiKey, visitorId) {
|
|
|
306
392
|
});
|
|
307
393
|
return _sessionFlight;
|
|
308
394
|
}
|
|
309
|
-
function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
395
|
+
function useAnalytics(apiUrl, apiKey, currentPath, wsUrl, routeName) {
|
|
310
396
|
const sessionId = useRef(null);
|
|
311
397
|
const pendingEvents = useRef([]);
|
|
312
398
|
const hasSentEnd = useRef(false);
|
|
@@ -383,7 +469,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
|
383
469
|
if (wsUrl) {
|
|
384
470
|
presenceRef.current = connectPresence(apiKey, id, wsUrl);
|
|
385
471
|
}
|
|
386
|
-
trackPageview(apiUrl, apiKey, id, currentPathRef.current);
|
|
472
|
+
trackPageview(apiUrl, apiKey, id, currentPathRef.current, routeName ?? null);
|
|
387
473
|
});
|
|
388
474
|
};
|
|
389
475
|
const handleStorage = (e) => {
|
|
@@ -399,7 +485,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
|
399
485
|
if (hasSentEnd.current || !sessionId.current) {
|
|
400
486
|
restartSession();
|
|
401
487
|
} else {
|
|
402
|
-
trackPageview(apiUrl, apiKey, sessionId.current, currentPathRef.current);
|
|
488
|
+
trackPageview(apiUrl, apiKey, sessionId.current, currentPathRef.current, routeName ?? null);
|
|
403
489
|
}
|
|
404
490
|
};
|
|
405
491
|
let visHideTimer = null;
|
|
@@ -437,7 +523,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
|
437
523
|
}
|
|
438
524
|
return {
|
|
439
525
|
trackPageview: (path) => {
|
|
440
|
-
enqueueOrRun(() => trackPageview(apiUrl, apiKey, sessionId.current, path));
|
|
526
|
+
enqueueOrRun(() => trackPageview(apiUrl, apiKey, sessionId.current, path, routeName ?? null));
|
|
441
527
|
},
|
|
442
528
|
trackClick: (e, element) => {
|
|
443
529
|
enqueueOrRun(() => trackClick(apiUrl, apiKey, sessionId.current, currentPath, e, element));
|
|
@@ -459,22 +545,40 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
|
459
545
|
},
|
|
460
546
|
trackSearch: (query) => {
|
|
461
547
|
enqueueOrRun(() => trackSearch(apiUrl, apiKey, sessionId.current, currentPath, query));
|
|
548
|
+
},
|
|
549
|
+
trackWebVital: (metric) => {
|
|
550
|
+
enqueueOrRun(() => trackWebVital(apiUrl, apiKey, sessionId.current, currentPath, metric));
|
|
462
551
|
}
|
|
463
552
|
};
|
|
464
553
|
}
|
|
465
554
|
|
|
466
555
|
// src/components/Analytics.tsx
|
|
467
|
-
function Analytics({ apiKey, apiUrl, wsUrl }) {
|
|
556
|
+
function Analytics({ apiKey, apiUrl, wsUrl, routeName }) {
|
|
468
557
|
const pathname = usePathname();
|
|
469
|
-
const { trackPageview: trackPageview2, trackClick: trackClick2, trackScroll: trackScroll2, trackCopy: trackCopy2, trackException: trackException2 } = useAnalytics(apiUrl, apiKey, pathname, wsUrl);
|
|
558
|
+
const { trackPageview: trackPageview2, trackClick: trackClick2, trackScroll: trackScroll2, trackCopy: trackCopy2, trackException: trackException2, trackWebVital: trackWebVital2 } = useAnalytics(apiUrl, apiKey, pathname, wsUrl, routeName);
|
|
470
559
|
const lastTracked = useRef2(null);
|
|
471
560
|
const lastScrollDepth = useRef2(0);
|
|
561
|
+
const trackWebVitalRef = useRef2(trackWebVital2);
|
|
562
|
+
useEffect2(() => {
|
|
563
|
+
trackWebVitalRef.current = trackWebVital2;
|
|
564
|
+
});
|
|
472
565
|
useEffect2(() => {
|
|
473
566
|
if (lastTracked.current === pathname) return;
|
|
474
567
|
lastTracked.current = pathname;
|
|
475
568
|
lastScrollDepth.current = 0;
|
|
476
|
-
trackPageview2(pathname);
|
|
569
|
+
setTimeout(() => trackPageview2(pathname), 0);
|
|
477
570
|
}, [pathname, trackPageview2]);
|
|
571
|
+
useEffect2(() => {
|
|
572
|
+
if (typeof window === "undefined") return;
|
|
573
|
+
const report = (name, value, rating) => {
|
|
574
|
+
trackWebVitalRef.current({ name, value, rating });
|
|
575
|
+
};
|
|
576
|
+
onLCP((m) => report("lcp_ms", Math.round(m.value), m.rating));
|
|
577
|
+
onCLS((m) => report("cls_score", m.value, m.rating));
|
|
578
|
+
onINP((m) => report("inp_ms", Math.round(m.value), m.rating));
|
|
579
|
+
onTTFB((m) => report("ttfb_ms", Math.round(m.value), m.rating));
|
|
580
|
+
onFCP((m) => report("fcp_ms", Math.round(m.value), m.rating));
|
|
581
|
+
}, []);
|
|
478
582
|
useEffect2(() => {
|
|
479
583
|
if (typeof window === "undefined") return;
|
|
480
584
|
const handler = (e) => trackClick2(e, e.target);
|
|
@@ -508,7 +612,10 @@ function Analytics({ apiKey, apiUrl, wsUrl }) {
|
|
|
508
612
|
const handler = (e) => trackException2({
|
|
509
613
|
exception_type: e.error?.name ?? "Error",
|
|
510
614
|
exception_message: e.message,
|
|
511
|
-
stack_trace: e.error?.stack ?? null
|
|
615
|
+
stack_trace: e.error?.stack ?? null,
|
|
616
|
+
exception_filename: e.filename || null,
|
|
617
|
+
exception_lineno: e.lineno || null,
|
|
618
|
+
exception_colno: e.colno || null
|
|
512
619
|
});
|
|
513
620
|
window.addEventListener("error", handler);
|
|
514
621
|
return () => window.removeEventListener("error", handler);
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ declare function isOptedOut(): boolean;
|
|
|
8
8
|
declare function consent(enabled: boolean): void;
|
|
9
9
|
|
|
10
10
|
declare function createSession(apiUrl: string, apiKey: string, visitorId?: string): Promise<string | null>;
|
|
11
|
-
declare function trackPageview(apiUrl: string, apiKey: string, sessionId: string, path: string): Promise<void>;
|
|
11
|
+
declare function trackPageview(apiUrl: string, apiKey: string, sessionId: string, path: string, routeName?: string | null): Promise<void>;
|
|
12
12
|
declare function trackClick(apiUrl: string, apiKey: string, sessionId: string, path: string, e: MouseEvent, element: HTMLElement): Promise<void>;
|
|
13
13
|
declare function trackScroll(apiUrl: string, apiKey: string, sessionId: string, path: string, depth: number): Promise<void>;
|
|
14
14
|
declare function trackCopy(apiUrl: string, apiKey: string, sessionId: string, path: string): Promise<void>;
|
|
@@ -17,14 +17,22 @@ interface ExceptionFields {
|
|
|
17
17
|
exception_type: string;
|
|
18
18
|
exception_message: string;
|
|
19
19
|
stack_trace: string | null;
|
|
20
|
+
exception_filename?: string | null;
|
|
21
|
+
exception_lineno?: number | null;
|
|
22
|
+
exception_colno?: number | null;
|
|
20
23
|
}
|
|
21
24
|
declare function trackException(apiUrl: string, apiKey: string, sessionId: string, path: string, fields: ExceptionFields): Promise<void>;
|
|
22
25
|
declare function trackError(apiUrl: string, apiKey: string, sessionId: string, path: string, error: Error): Promise<void>;
|
|
23
26
|
declare function trackCTA(apiUrl: string, apiKey: string, sessionId: string, path: string, ctaId: string, ctaVariant?: string): Promise<void>;
|
|
27
|
+
declare function trackWebVital(apiUrl: string, apiKey: string, sessionId: string, path: string, metric: {
|
|
28
|
+
name: string;
|
|
29
|
+
value: number;
|
|
30
|
+
rating: string;
|
|
31
|
+
}): Promise<void>;
|
|
24
32
|
|
|
25
33
|
interface PresenceConnection {
|
|
26
34
|
disconnect: () => void;
|
|
27
35
|
}
|
|
28
36
|
declare function connectPresence(apiKey: string, sessionId: string, wsUrl: string): PresenceConnection;
|
|
29
37
|
|
|
30
|
-
export { type ExceptionFields, type PresenceConnection, connectPresence, consent, createSession, isOptedOut, optIn, optOut, trackCTA, trackClick, trackCopy, trackError, trackException, trackPageview, trackScroll, trackSearch };
|
|
38
|
+
export { type ExceptionFields, type PresenceConnection, connectPresence, consent, createSession, isOptedOut, optIn, optOut, trackCTA, trackClick, trackCopy, trackError, trackException, trackPageview, trackScroll, trackSearch, trackWebVital };
|
package/dist/index.js
CHANGED
|
@@ -19,12 +19,60 @@ function consent(enabled) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// src/lib/api.ts
|
|
22
|
-
var BASE_FIELDS = (sessionId, eventType, path) => ({
|
|
22
|
+
var BASE_FIELDS = (sessionId, eventType, path, eventName, eventCategory) => ({
|
|
23
23
|
session_id: sessionId,
|
|
24
24
|
event_type: eventType,
|
|
25
|
+
event_name: eventName ?? null,
|
|
26
|
+
event_category: eventCategory ?? null,
|
|
25
27
|
path,
|
|
26
|
-
page_url: typeof window !== "undefined" ? `${window.location.origin}${path}` : ""
|
|
28
|
+
page_url: typeof window !== "undefined" ? `${window.location.origin}${path}` : "",
|
|
29
|
+
// Page context
|
|
30
|
+
page_title: typeof document !== "undefined" ? document.title || null : null,
|
|
31
|
+
referrer: typeof document !== "undefined" ? document.referrer || null : null,
|
|
32
|
+
// Viewport
|
|
33
|
+
viewport_width: typeof window !== "undefined" ? window.innerWidth : null,
|
|
34
|
+
viewport_height: typeof window !== "undefined" ? window.innerHeight : null,
|
|
35
|
+
// Timing — how long since page loaded
|
|
36
|
+
time_on_page_ms: typeof performance !== "undefined" ? Math.round(performance.now()) : null,
|
|
37
|
+
// User engagement state at moment of event
|
|
38
|
+
document_visibility: typeof document !== "undefined" ? document.visibilityState : null
|
|
27
39
|
});
|
|
40
|
+
function getConnectionType() {
|
|
41
|
+
if (typeof navigator === "undefined") return null;
|
|
42
|
+
const nav = navigator;
|
|
43
|
+
return nav.connection?.effectiveType ?? null;
|
|
44
|
+
}
|
|
45
|
+
function resolveInteractiveTarget(el) {
|
|
46
|
+
const interactive = el.closest(
|
|
47
|
+
'a, button, [role="button"], input, select, textarea, label, summary'
|
|
48
|
+
);
|
|
49
|
+
return interactive ?? el;
|
|
50
|
+
}
|
|
51
|
+
function resolveElementHref(element) {
|
|
52
|
+
if (element.tagName.toLowerCase() === "a") {
|
|
53
|
+
return element.href || null;
|
|
54
|
+
}
|
|
55
|
+
const anchor = element.closest("a");
|
|
56
|
+
return anchor ? anchor.href || null : null;
|
|
57
|
+
}
|
|
58
|
+
function safeClassName(el) {
|
|
59
|
+
const cn = el.className;
|
|
60
|
+
return typeof cn === "string" ? cn || null : null;
|
|
61
|
+
}
|
|
62
|
+
function cleanLabel(raw, maxLen) {
|
|
63
|
+
if (!raw) return null;
|
|
64
|
+
const cleaned = raw.replace(/\s+/g, " ").trim().slice(0, maxLen);
|
|
65
|
+
return cleaned || null;
|
|
66
|
+
}
|
|
67
|
+
function isExternalHref(href) {
|
|
68
|
+
if (!href) return null;
|
|
69
|
+
if (typeof window === "undefined") return null;
|
|
70
|
+
try {
|
|
71
|
+
return new URL(href).origin !== window.location.origin;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
28
76
|
function getGpu() {
|
|
29
77
|
try {
|
|
30
78
|
const canvas = document.createElement("canvas");
|
|
@@ -63,7 +111,6 @@ async function createSession(apiUrl, apiKey, visitorId) {
|
|
|
63
111
|
sessionData.utm_term = params.get("utm_term");
|
|
64
112
|
sessionData.utm_content = params.get("utm_content");
|
|
65
113
|
}
|
|
66
|
-
console.log(`[AF Analytics SDK] Creating session at ${apiUrl}/sessions with API key: ${apiKey.slice(0, 10)}...`);
|
|
67
114
|
try {
|
|
68
115
|
const response = await fetch(`${apiUrl}/sessions`, {
|
|
69
116
|
method: "POST",
|
|
@@ -73,17 +120,10 @@ async function createSession(apiUrl, apiKey, visitorId) {
|
|
|
73
120
|
},
|
|
74
121
|
body: JSON.stringify(sessionData)
|
|
75
122
|
});
|
|
76
|
-
|
|
77
|
-
if (!response.ok) {
|
|
78
|
-
const errText = await response.text();
|
|
79
|
-
console.error(`[AF Analytics SDK] Session creation failed: ${errText}`);
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
123
|
+
if (!response.ok) return null;
|
|
82
124
|
const data = await response.json();
|
|
83
|
-
console.log(`[AF Analytics SDK] Session created: ${data.id}`);
|
|
84
125
|
return data.id ?? null;
|
|
85
|
-
} catch
|
|
86
|
-
console.error(`[AF Analytics SDK] Session creation error: ${err}`);
|
|
126
|
+
} catch {
|
|
87
127
|
return null;
|
|
88
128
|
}
|
|
89
129
|
}
|
|
@@ -98,45 +138,73 @@ async function sendEvent(apiUrl, apiKey, payload) {
|
|
|
98
138
|
body: JSON.stringify(payload)
|
|
99
139
|
});
|
|
100
140
|
}
|
|
101
|
-
async function trackPageview(apiUrl, apiKey, sessionId, path) {
|
|
141
|
+
async function trackPageview(apiUrl, apiKey, sessionId, path, routeName) {
|
|
142
|
+
const navigationEntry = typeof performance !== "undefined" ? performance.getEntriesByType("navigation")[0] : void 0;
|
|
143
|
+
const title = typeof document !== "undefined" ? document.title || null : null;
|
|
144
|
+
const eventName = title || path;
|
|
102
145
|
await sendEvent(apiUrl, apiKey, {
|
|
103
|
-
...BASE_FIELDS(sessionId, "pageview", path),
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
146
|
+
...BASE_FIELDS(sessionId, "pageview", path, eventName, "navigation"),
|
|
147
|
+
scroll_depth: 0,
|
|
148
|
+
route_name: routeName ?? null,
|
|
149
|
+
navigation_type: navigationEntry?.type ?? null,
|
|
150
|
+
connection_type: getConnectionType()
|
|
107
151
|
});
|
|
108
152
|
}
|
|
109
153
|
async function trackClick(apiUrl, apiKey, sessionId, path, e, element) {
|
|
154
|
+
const target = resolveInteractiveTarget(element);
|
|
155
|
+
const href = resolveElementHref(target);
|
|
156
|
+
const ariaLabel = target.getAttribute("aria-label");
|
|
157
|
+
const rawText = target.innerText ?? target.textContent ?? void 0;
|
|
158
|
+
const eventName = cleanLabel(ariaLabel, 60) ?? cleanLabel(rawText, 60) ?? target.tagName.toLowerCase();
|
|
110
159
|
await sendEvent(apiUrl, apiKey, {
|
|
111
|
-
...BASE_FIELDS(sessionId, "click", path),
|
|
160
|
+
...BASE_FIELDS(sessionId, "click", path, eventName, "interaction"),
|
|
112
161
|
x_position: e.clientX,
|
|
113
162
|
y_position: e.clientY,
|
|
114
|
-
element_id:
|
|
115
|
-
element_class:
|
|
116
|
-
element_text:
|
|
163
|
+
element_id: target.id || null,
|
|
164
|
+
element_class: safeClassName(target),
|
|
165
|
+
element_text: cleanLabel(rawText, 100),
|
|
166
|
+
tag_name: target.tagName.toLowerCase(),
|
|
167
|
+
element_href: href,
|
|
168
|
+
element_role: target.getAttribute("role") || null,
|
|
169
|
+
element_aria_label: ariaLabel || null,
|
|
170
|
+
element_type: target.type || null,
|
|
171
|
+
element_name: target.name || null,
|
|
172
|
+
is_external_link: isExternalHref(href),
|
|
173
|
+
modifier_keys: {
|
|
174
|
+
alt: e.altKey,
|
|
175
|
+
ctrl: e.ctrlKey,
|
|
176
|
+
meta: e.metaKey,
|
|
177
|
+
shift: e.shiftKey
|
|
178
|
+
},
|
|
179
|
+
click_count: e.detail
|
|
117
180
|
});
|
|
118
181
|
}
|
|
119
182
|
async function trackScroll(apiUrl, apiKey, sessionId, path, depth) {
|
|
120
183
|
await sendEvent(apiUrl, apiKey, {
|
|
121
|
-
...BASE_FIELDS(sessionId, "scroll", path),
|
|
122
|
-
scroll_depth: depth
|
|
184
|
+
...BASE_FIELDS(sessionId, "scroll", path, `Scroll ${depth}%`, "interaction"),
|
|
185
|
+
scroll_depth: depth,
|
|
186
|
+
scroll_y_px: typeof window !== "undefined" ? window.scrollY : null,
|
|
187
|
+
document_height_px: typeof document !== "undefined" ? document.documentElement.scrollHeight : null
|
|
123
188
|
});
|
|
124
189
|
}
|
|
125
190
|
async function trackCopy(apiUrl, apiKey, sessionId, path) {
|
|
191
|
+
const selection = typeof window !== "undefined" ? window.getSelection()?.toString() ?? null : null;
|
|
126
192
|
await sendEvent(apiUrl, apiKey, {
|
|
127
|
-
...BASE_FIELDS(sessionId, "copy", path),
|
|
128
|
-
copied_text:
|
|
193
|
+
...BASE_FIELDS(sessionId, "copy", path, "Copy", "interaction"),
|
|
194
|
+
copied_text: selection?.slice(0, 200) || null,
|
|
195
|
+
selection_length: selection?.length ?? 0
|
|
129
196
|
});
|
|
130
197
|
}
|
|
131
198
|
async function trackSearch(apiUrl, apiKey, sessionId, path, query) {
|
|
132
199
|
await sendEvent(apiUrl, apiKey, {
|
|
133
|
-
...BASE_FIELDS(sessionId, "search", path),
|
|
200
|
+
...BASE_FIELDS(sessionId, "search", path, `Search: ${query.slice(0, 60)}`, "interaction"),
|
|
134
201
|
search_query: query
|
|
135
202
|
});
|
|
136
203
|
}
|
|
137
204
|
async function trackException(apiUrl, apiKey, sessionId, path, fields) {
|
|
205
|
+
const eventName = fields.exception_type ? `${fields.exception_type}: ${fields.exception_message.slice(0, 60)}` : fields.exception_message.slice(0, 80);
|
|
138
206
|
await sendEvent(apiUrl, apiKey, {
|
|
139
|
-
...BASE_FIELDS(sessionId, "js_exception", path),
|
|
207
|
+
...BASE_FIELDS(sessionId, "js_exception", path, eventName, "error"),
|
|
140
208
|
...fields
|
|
141
209
|
});
|
|
142
210
|
}
|
|
@@ -148,12 +216,29 @@ async function trackError(apiUrl, apiKey, sessionId, path, error) {
|
|
|
148
216
|
});
|
|
149
217
|
}
|
|
150
218
|
async function trackCTA(apiUrl, apiKey, sessionId, path, ctaId, ctaVariant) {
|
|
219
|
+
const eventName = ctaVariant ? `${ctaId} (${ctaVariant})` : ctaId;
|
|
151
220
|
await sendEvent(apiUrl, apiKey, {
|
|
152
|
-
...BASE_FIELDS(sessionId, "cta_click", path),
|
|
221
|
+
...BASE_FIELDS(sessionId, "cta_click", path, eventName, "conversion"),
|
|
153
222
|
cta_id: ctaId,
|
|
154
223
|
cta_variant: ctaVariant || null
|
|
155
224
|
});
|
|
156
225
|
}
|
|
226
|
+
var WEB_VITAL_FIELDS = {
|
|
227
|
+
lcp_ms: "lcp_ms",
|
|
228
|
+
cls_score: "cls_score",
|
|
229
|
+
inp_ms: "inp_ms",
|
|
230
|
+
ttfb_ms: "ttfb_ms",
|
|
231
|
+
fcp_ms: "fcp_ms"
|
|
232
|
+
};
|
|
233
|
+
async function trackWebVital(apiUrl, apiKey, sessionId, path, metric) {
|
|
234
|
+
const field = WEB_VITAL_FIELDS[metric.name];
|
|
235
|
+
if (!field) return;
|
|
236
|
+
await sendEvent(apiUrl, apiKey, {
|
|
237
|
+
...BASE_FIELDS(sessionId, "web_vital", path, metric.name.toUpperCase(), "performance"),
|
|
238
|
+
[field]: metric.value,
|
|
239
|
+
rating: metric.rating
|
|
240
|
+
});
|
|
241
|
+
}
|
|
157
242
|
|
|
158
243
|
// src/lib/presence.ts
|
|
159
244
|
function connectPresence(apiKey, sessionId, wsUrl) {
|
|
@@ -238,5 +323,6 @@ export {
|
|
|
238
323
|
trackException,
|
|
239
324
|
trackPageview,
|
|
240
325
|
trackScroll,
|
|
241
|
-
trackSearch
|
|
326
|
+
trackSearch,
|
|
327
|
+
trackWebVital
|
|
242
328
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arthurreira/analytics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsup",
|
|
7
|
+
"prepare": "tsup",
|
|
8
|
+
"dev": "tsup src/index.ts --format esm --dts --watch"
|
|
9
|
+
},
|
|
5
10
|
"exports": {
|
|
6
11
|
".": {
|
|
7
12
|
"types": "./dist/index.d.ts",
|
|
@@ -29,12 +34,11 @@
|
|
|
29
34
|
"typescript": "^5.9.2",
|
|
30
35
|
"vitest": "^4.1.7"
|
|
31
36
|
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"web-vitals": "^5.1.0"
|
|
39
|
+
},
|
|
32
40
|
"peerDependencies": {
|
|
33
41
|
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
34
42
|
"react": "^18.0.0 || ^19.0.0"
|
|
35
|
-
},
|
|
36
|
-
"scripts": {
|
|
37
|
-
"build": "tsup",
|
|
38
|
-
"dev": "tsup src/index.ts --format esm --dts --watch"
|
|
39
43
|
}
|
|
40
44
|
}
|