@arthurreira/analytics 0.20.0 → 0.21.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 +121 -34
- package/dist/index.d.ts +10 -2
- package/dist/index.js +97 -27
- 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,45 @@ 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 resolveElementHref(element) {
|
|
45
|
+
if (element.tagName.toLowerCase() === "a") {
|
|
46
|
+
return element.href || null;
|
|
47
|
+
}
|
|
48
|
+
const anchor = element.closest("a");
|
|
49
|
+
return anchor ? anchor.href || null : null;
|
|
50
|
+
}
|
|
51
|
+
function isExternalHref(href) {
|
|
52
|
+
if (!href) return null;
|
|
53
|
+
if (typeof window === "undefined") return null;
|
|
54
|
+
try {
|
|
55
|
+
return new URL(href).origin !== window.location.origin;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
26
60
|
function getGpu() {
|
|
27
61
|
try {
|
|
28
62
|
const canvas = document.createElement("canvas");
|
|
@@ -61,7 +95,6 @@ async function createSession(apiUrl, apiKey, visitorId) {
|
|
|
61
95
|
sessionData.utm_term = params.get("utm_term");
|
|
62
96
|
sessionData.utm_content = params.get("utm_content");
|
|
63
97
|
}
|
|
64
|
-
console.log(`[AF Analytics SDK] Creating session at ${apiUrl}/sessions with API key: ${apiKey.slice(0, 10)}...`);
|
|
65
98
|
try {
|
|
66
99
|
const response = await fetch(`${apiUrl}/sessions`, {
|
|
67
100
|
method: "POST",
|
|
@@ -71,17 +104,10 @@ async function createSession(apiUrl, apiKey, visitorId) {
|
|
|
71
104
|
},
|
|
72
105
|
body: JSON.stringify(sessionData)
|
|
73
106
|
});
|
|
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
|
-
}
|
|
107
|
+
if (!response.ok) return null;
|
|
80
108
|
const data = await response.json();
|
|
81
|
-
console.log(`[AF Analytics SDK] Session created: ${data.id}`);
|
|
82
109
|
return data.id ?? null;
|
|
83
|
-
} catch
|
|
84
|
-
console.error(`[AF Analytics SDK] Session creation error: ${err}`);
|
|
110
|
+
} catch {
|
|
85
111
|
return null;
|
|
86
112
|
}
|
|
87
113
|
}
|
|
@@ -96,45 +122,72 @@ async function sendEvent(apiUrl, apiKey, payload) {
|
|
|
96
122
|
body: JSON.stringify(payload)
|
|
97
123
|
});
|
|
98
124
|
}
|
|
99
|
-
async function trackPageview(apiUrl, apiKey, sessionId, path) {
|
|
125
|
+
async function trackPageview(apiUrl, apiKey, sessionId, path, routeName) {
|
|
126
|
+
const navigationEntry = typeof performance !== "undefined" ? performance.getEntriesByType("navigation")[0] : void 0;
|
|
127
|
+
const title = typeof document !== "undefined" ? document.title || null : null;
|
|
128
|
+
const eventName = title || path;
|
|
100
129
|
await sendEvent(apiUrl, apiKey, {
|
|
101
|
-
...BASE_FIELDS(sessionId, "pageview", path),
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
130
|
+
...BASE_FIELDS(sessionId, "pageview", path, eventName, "navigation"),
|
|
131
|
+
scroll_depth: 0,
|
|
132
|
+
route_name: routeName ?? null,
|
|
133
|
+
navigation_type: navigationEntry?.type ?? null,
|
|
134
|
+
connection_type: getConnectionType()
|
|
105
135
|
});
|
|
106
136
|
}
|
|
107
137
|
async function trackClick(apiUrl, apiKey, sessionId, path, e, element) {
|
|
138
|
+
const href = resolveElementHref(element);
|
|
139
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
140
|
+
const text = element.innerText?.slice(0, 60);
|
|
141
|
+
const eventName = ariaLabel || text || element.tagName.toLowerCase();
|
|
108
142
|
await sendEvent(apiUrl, apiKey, {
|
|
109
|
-
...BASE_FIELDS(sessionId, "click", path),
|
|
143
|
+
...BASE_FIELDS(sessionId, "click", path, eventName, "interaction"),
|
|
110
144
|
x_position: e.clientX,
|
|
111
145
|
y_position: e.clientY,
|
|
112
146
|
element_id: element.id || null,
|
|
113
147
|
element_class: element.className || null,
|
|
114
|
-
element_text: element.innerText?.slice(0, 100) || null
|
|
148
|
+
element_text: element.innerText?.slice(0, 100) || null,
|
|
149
|
+
tag_name: element.tagName.toLowerCase(),
|
|
150
|
+
element_href: href,
|
|
151
|
+
element_role: element.getAttribute("role") || null,
|
|
152
|
+
element_aria_label: element.getAttribute("aria-label") || null,
|
|
153
|
+
element_type: element.type || null,
|
|
154
|
+
element_name: element.name || null,
|
|
155
|
+
is_external_link: isExternalHref(href),
|
|
156
|
+
modifier_keys: {
|
|
157
|
+
alt: e.altKey,
|
|
158
|
+
ctrl: e.ctrlKey,
|
|
159
|
+
meta: e.metaKey,
|
|
160
|
+
shift: e.shiftKey
|
|
161
|
+
},
|
|
162
|
+
click_count: e.detail
|
|
115
163
|
});
|
|
116
164
|
}
|
|
117
165
|
async function trackScroll(apiUrl, apiKey, sessionId, path, depth) {
|
|
118
166
|
await sendEvent(apiUrl, apiKey, {
|
|
119
|
-
...BASE_FIELDS(sessionId, "scroll", path),
|
|
120
|
-
scroll_depth: depth
|
|
167
|
+
...BASE_FIELDS(sessionId, "scroll", path, `Scroll ${depth}%`, "interaction"),
|
|
168
|
+
scroll_depth: depth,
|
|
169
|
+
scroll_y_px: typeof window !== "undefined" ? window.scrollY : null,
|
|
170
|
+
document_height_px: typeof document !== "undefined" ? document.documentElement.scrollHeight : null
|
|
121
171
|
});
|
|
122
172
|
}
|
|
123
173
|
async function trackCopy(apiUrl, apiKey, sessionId, path) {
|
|
174
|
+
const selection = typeof window !== "undefined" ? window.getSelection()?.toString() ?? null : null;
|
|
124
175
|
await sendEvent(apiUrl, apiKey, {
|
|
125
|
-
...BASE_FIELDS(sessionId, "copy", path),
|
|
126
|
-
copied_text:
|
|
176
|
+
...BASE_FIELDS(sessionId, "copy", path, "Copy", "interaction"),
|
|
177
|
+
copied_text: selection?.slice(0, 200) || null,
|
|
178
|
+
selection_length: selection?.length ?? 0
|
|
127
179
|
});
|
|
128
180
|
}
|
|
129
181
|
async function trackSearch(apiUrl, apiKey, sessionId, path, query) {
|
|
130
182
|
await sendEvent(apiUrl, apiKey, {
|
|
131
|
-
...BASE_FIELDS(sessionId, "search", path),
|
|
183
|
+
...BASE_FIELDS(sessionId, "search", path, `Search: ${query.slice(0, 60)}`, "interaction"),
|
|
132
184
|
search_query: query
|
|
133
185
|
});
|
|
134
186
|
}
|
|
135
187
|
async function trackException(apiUrl, apiKey, sessionId, path, fields) {
|
|
188
|
+
const eventName = fields.exception_type ? `${fields.exception_type}: ${fields.exception_message.slice(0, 60)}` : fields.exception_message.slice(0, 80);
|
|
136
189
|
await sendEvent(apiUrl, apiKey, {
|
|
137
|
-
...BASE_FIELDS(sessionId, "js_exception", path),
|
|
190
|
+
...BASE_FIELDS(sessionId, "js_exception", path, eventName, "error"),
|
|
138
191
|
...fields
|
|
139
192
|
});
|
|
140
193
|
}
|
|
@@ -146,12 +199,29 @@ async function trackError(apiUrl, apiKey, sessionId, path, error) {
|
|
|
146
199
|
});
|
|
147
200
|
}
|
|
148
201
|
async function trackCTA(apiUrl, apiKey, sessionId, path, ctaId, ctaVariant) {
|
|
202
|
+
const eventName = ctaVariant ? `${ctaId} (${ctaVariant})` : ctaId;
|
|
149
203
|
await sendEvent(apiUrl, apiKey, {
|
|
150
|
-
...BASE_FIELDS(sessionId, "cta_click", path),
|
|
204
|
+
...BASE_FIELDS(sessionId, "cta_click", path, eventName, "conversion"),
|
|
151
205
|
cta_id: ctaId,
|
|
152
206
|
cta_variant: ctaVariant || null
|
|
153
207
|
});
|
|
154
208
|
}
|
|
209
|
+
var WEB_VITAL_FIELDS = {
|
|
210
|
+
lcp_ms: "lcp_ms",
|
|
211
|
+
cls_score: "cls_score",
|
|
212
|
+
inp_ms: "inp_ms",
|
|
213
|
+
ttfb_ms: "ttfb_ms",
|
|
214
|
+
fcp_ms: "fcp_ms"
|
|
215
|
+
};
|
|
216
|
+
async function trackWebVital(apiUrl, apiKey, sessionId, path, metric) {
|
|
217
|
+
const field = WEB_VITAL_FIELDS[metric.name];
|
|
218
|
+
if (!field) return;
|
|
219
|
+
await sendEvent(apiUrl, apiKey, {
|
|
220
|
+
...BASE_FIELDS(sessionId, "web_vital", path, metric.name.toUpperCase(), "performance"),
|
|
221
|
+
[field]: metric.value,
|
|
222
|
+
rating: metric.rating
|
|
223
|
+
});
|
|
224
|
+
}
|
|
155
225
|
|
|
156
226
|
// src/lib/presence.ts
|
|
157
227
|
function connectPresence(apiKey, sessionId, wsUrl) {
|
|
@@ -306,7 +376,7 @@ async function getOrCreateSession(apiUrl, apiKey, visitorId) {
|
|
|
306
376
|
});
|
|
307
377
|
return _sessionFlight;
|
|
308
378
|
}
|
|
309
|
-
function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
379
|
+
function useAnalytics(apiUrl, apiKey, currentPath, wsUrl, routeName) {
|
|
310
380
|
const sessionId = useRef(null);
|
|
311
381
|
const pendingEvents = useRef([]);
|
|
312
382
|
const hasSentEnd = useRef(false);
|
|
@@ -383,7 +453,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
|
383
453
|
if (wsUrl) {
|
|
384
454
|
presenceRef.current = connectPresence(apiKey, id, wsUrl);
|
|
385
455
|
}
|
|
386
|
-
trackPageview(apiUrl, apiKey, id, currentPathRef.current);
|
|
456
|
+
trackPageview(apiUrl, apiKey, id, currentPathRef.current, routeName ?? null);
|
|
387
457
|
});
|
|
388
458
|
};
|
|
389
459
|
const handleStorage = (e) => {
|
|
@@ -399,7 +469,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
|
399
469
|
if (hasSentEnd.current || !sessionId.current) {
|
|
400
470
|
restartSession();
|
|
401
471
|
} else {
|
|
402
|
-
trackPageview(apiUrl, apiKey, sessionId.current, currentPathRef.current);
|
|
472
|
+
trackPageview(apiUrl, apiKey, sessionId.current, currentPathRef.current, routeName ?? null);
|
|
403
473
|
}
|
|
404
474
|
};
|
|
405
475
|
let visHideTimer = null;
|
|
@@ -437,7 +507,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
|
437
507
|
}
|
|
438
508
|
return {
|
|
439
509
|
trackPageview: (path) => {
|
|
440
|
-
enqueueOrRun(() => trackPageview(apiUrl, apiKey, sessionId.current, path));
|
|
510
|
+
enqueueOrRun(() => trackPageview(apiUrl, apiKey, sessionId.current, path, routeName ?? null));
|
|
441
511
|
},
|
|
442
512
|
trackClick: (e, element) => {
|
|
443
513
|
enqueueOrRun(() => trackClick(apiUrl, apiKey, sessionId.current, currentPath, e, element));
|
|
@@ -459,22 +529,36 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
|
459
529
|
},
|
|
460
530
|
trackSearch: (query) => {
|
|
461
531
|
enqueueOrRun(() => trackSearch(apiUrl, apiKey, sessionId.current, currentPath, query));
|
|
532
|
+
},
|
|
533
|
+
trackWebVital: (metric) => {
|
|
534
|
+
enqueueOrRun(() => trackWebVital(apiUrl, apiKey, sessionId.current, currentPath, metric));
|
|
462
535
|
}
|
|
463
536
|
};
|
|
464
537
|
}
|
|
465
538
|
|
|
466
539
|
// src/components/Analytics.tsx
|
|
467
|
-
function Analytics({ apiKey, apiUrl, wsUrl }) {
|
|
540
|
+
function Analytics({ apiKey, apiUrl, wsUrl, routeName }) {
|
|
468
541
|
const pathname = usePathname();
|
|
469
|
-
const { trackPageview: trackPageview2, trackClick: trackClick2, trackScroll: trackScroll2, trackCopy: trackCopy2, trackException: trackException2 } = useAnalytics(apiUrl, apiKey, pathname, wsUrl);
|
|
542
|
+
const { trackPageview: trackPageview2, trackClick: trackClick2, trackScroll: trackScroll2, trackCopy: trackCopy2, trackException: trackException2, trackWebVital: trackWebVital2 } = useAnalytics(apiUrl, apiKey, pathname, wsUrl, routeName);
|
|
470
543
|
const lastTracked = useRef2(null);
|
|
471
544
|
const lastScrollDepth = useRef2(0);
|
|
472
545
|
useEffect2(() => {
|
|
473
546
|
if (lastTracked.current === pathname) return;
|
|
474
547
|
lastTracked.current = pathname;
|
|
475
548
|
lastScrollDepth.current = 0;
|
|
476
|
-
trackPageview2(pathname);
|
|
549
|
+
setTimeout(() => trackPageview2(pathname), 0);
|
|
477
550
|
}, [pathname, trackPageview2]);
|
|
551
|
+
useEffect2(() => {
|
|
552
|
+
if (typeof window === "undefined") return;
|
|
553
|
+
const report = (name, value, rating) => {
|
|
554
|
+
trackWebVital2({ name, value, rating });
|
|
555
|
+
};
|
|
556
|
+
onLCP((m) => report("lcp_ms", Math.round(m.value), m.rating));
|
|
557
|
+
onCLS((m) => report("cls_score", m.value, m.rating));
|
|
558
|
+
onINP((m) => report("inp_ms", Math.round(m.value), m.rating));
|
|
559
|
+
onTTFB((m) => report("ttfb_ms", Math.round(m.value), m.rating));
|
|
560
|
+
onFCP((m) => report("fcp_ms", Math.round(m.value), m.rating));
|
|
561
|
+
}, [trackWebVital2]);
|
|
478
562
|
useEffect2(() => {
|
|
479
563
|
if (typeof window === "undefined") return;
|
|
480
564
|
const handler = (e) => trackClick2(e, e.target);
|
|
@@ -508,7 +592,10 @@ function Analytics({ apiKey, apiUrl, wsUrl }) {
|
|
|
508
592
|
const handler = (e) => trackException2({
|
|
509
593
|
exception_type: e.error?.name ?? "Error",
|
|
510
594
|
exception_message: e.message,
|
|
511
|
-
stack_trace: e.error?.stack ?? null
|
|
595
|
+
stack_trace: e.error?.stack ?? null,
|
|
596
|
+
exception_filename: e.filename || null,
|
|
597
|
+
exception_lineno: e.lineno || null,
|
|
598
|
+
exception_colno: e.colno || null
|
|
512
599
|
});
|
|
513
600
|
window.addEventListener("error", handler);
|
|
514
601
|
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,45 @@ 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 resolveElementHref(element) {
|
|
46
|
+
if (element.tagName.toLowerCase() === "a") {
|
|
47
|
+
return element.href || null;
|
|
48
|
+
}
|
|
49
|
+
const anchor = element.closest("a");
|
|
50
|
+
return anchor ? anchor.href || null : null;
|
|
51
|
+
}
|
|
52
|
+
function isExternalHref(href) {
|
|
53
|
+
if (!href) return null;
|
|
54
|
+
if (typeof window === "undefined") return null;
|
|
55
|
+
try {
|
|
56
|
+
return new URL(href).origin !== window.location.origin;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
28
61
|
function getGpu() {
|
|
29
62
|
try {
|
|
30
63
|
const canvas = document.createElement("canvas");
|
|
@@ -63,7 +96,6 @@ async function createSession(apiUrl, apiKey, visitorId) {
|
|
|
63
96
|
sessionData.utm_term = params.get("utm_term");
|
|
64
97
|
sessionData.utm_content = params.get("utm_content");
|
|
65
98
|
}
|
|
66
|
-
console.log(`[AF Analytics SDK] Creating session at ${apiUrl}/sessions with API key: ${apiKey.slice(0, 10)}...`);
|
|
67
99
|
try {
|
|
68
100
|
const response = await fetch(`${apiUrl}/sessions`, {
|
|
69
101
|
method: "POST",
|
|
@@ -73,17 +105,10 @@ async function createSession(apiUrl, apiKey, visitorId) {
|
|
|
73
105
|
},
|
|
74
106
|
body: JSON.stringify(sessionData)
|
|
75
107
|
});
|
|
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
|
-
}
|
|
108
|
+
if (!response.ok) return null;
|
|
82
109
|
const data = await response.json();
|
|
83
|
-
console.log(`[AF Analytics SDK] Session created: ${data.id}`);
|
|
84
110
|
return data.id ?? null;
|
|
85
|
-
} catch
|
|
86
|
-
console.error(`[AF Analytics SDK] Session creation error: ${err}`);
|
|
111
|
+
} catch {
|
|
87
112
|
return null;
|
|
88
113
|
}
|
|
89
114
|
}
|
|
@@ -98,45 +123,72 @@ async function sendEvent(apiUrl, apiKey, payload) {
|
|
|
98
123
|
body: JSON.stringify(payload)
|
|
99
124
|
});
|
|
100
125
|
}
|
|
101
|
-
async function trackPageview(apiUrl, apiKey, sessionId, path) {
|
|
126
|
+
async function trackPageview(apiUrl, apiKey, sessionId, path, routeName) {
|
|
127
|
+
const navigationEntry = typeof performance !== "undefined" ? performance.getEntriesByType("navigation")[0] : void 0;
|
|
128
|
+
const title = typeof document !== "undefined" ? document.title || null : null;
|
|
129
|
+
const eventName = title || path;
|
|
102
130
|
await sendEvent(apiUrl, apiKey, {
|
|
103
|
-
...BASE_FIELDS(sessionId, "pageview", path),
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
131
|
+
...BASE_FIELDS(sessionId, "pageview", path, eventName, "navigation"),
|
|
132
|
+
scroll_depth: 0,
|
|
133
|
+
route_name: routeName ?? null,
|
|
134
|
+
navigation_type: navigationEntry?.type ?? null,
|
|
135
|
+
connection_type: getConnectionType()
|
|
107
136
|
});
|
|
108
137
|
}
|
|
109
138
|
async function trackClick(apiUrl, apiKey, sessionId, path, e, element) {
|
|
139
|
+
const href = resolveElementHref(element);
|
|
140
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
141
|
+
const text = element.innerText?.slice(0, 60);
|
|
142
|
+
const eventName = ariaLabel || text || element.tagName.toLowerCase();
|
|
110
143
|
await sendEvent(apiUrl, apiKey, {
|
|
111
|
-
...BASE_FIELDS(sessionId, "click", path),
|
|
144
|
+
...BASE_FIELDS(sessionId, "click", path, eventName, "interaction"),
|
|
112
145
|
x_position: e.clientX,
|
|
113
146
|
y_position: e.clientY,
|
|
114
147
|
element_id: element.id || null,
|
|
115
148
|
element_class: element.className || null,
|
|
116
|
-
element_text: element.innerText?.slice(0, 100) || null
|
|
149
|
+
element_text: element.innerText?.slice(0, 100) || null,
|
|
150
|
+
tag_name: element.tagName.toLowerCase(),
|
|
151
|
+
element_href: href,
|
|
152
|
+
element_role: element.getAttribute("role") || null,
|
|
153
|
+
element_aria_label: element.getAttribute("aria-label") || null,
|
|
154
|
+
element_type: element.type || null,
|
|
155
|
+
element_name: element.name || null,
|
|
156
|
+
is_external_link: isExternalHref(href),
|
|
157
|
+
modifier_keys: {
|
|
158
|
+
alt: e.altKey,
|
|
159
|
+
ctrl: e.ctrlKey,
|
|
160
|
+
meta: e.metaKey,
|
|
161
|
+
shift: e.shiftKey
|
|
162
|
+
},
|
|
163
|
+
click_count: e.detail
|
|
117
164
|
});
|
|
118
165
|
}
|
|
119
166
|
async function trackScroll(apiUrl, apiKey, sessionId, path, depth) {
|
|
120
167
|
await sendEvent(apiUrl, apiKey, {
|
|
121
|
-
...BASE_FIELDS(sessionId, "scroll", path),
|
|
122
|
-
scroll_depth: depth
|
|
168
|
+
...BASE_FIELDS(sessionId, "scroll", path, `Scroll ${depth}%`, "interaction"),
|
|
169
|
+
scroll_depth: depth,
|
|
170
|
+
scroll_y_px: typeof window !== "undefined" ? window.scrollY : null,
|
|
171
|
+
document_height_px: typeof document !== "undefined" ? document.documentElement.scrollHeight : null
|
|
123
172
|
});
|
|
124
173
|
}
|
|
125
174
|
async function trackCopy(apiUrl, apiKey, sessionId, path) {
|
|
175
|
+
const selection = typeof window !== "undefined" ? window.getSelection()?.toString() ?? null : null;
|
|
126
176
|
await sendEvent(apiUrl, apiKey, {
|
|
127
|
-
...BASE_FIELDS(sessionId, "copy", path),
|
|
128
|
-
copied_text:
|
|
177
|
+
...BASE_FIELDS(sessionId, "copy", path, "Copy", "interaction"),
|
|
178
|
+
copied_text: selection?.slice(0, 200) || null,
|
|
179
|
+
selection_length: selection?.length ?? 0
|
|
129
180
|
});
|
|
130
181
|
}
|
|
131
182
|
async function trackSearch(apiUrl, apiKey, sessionId, path, query) {
|
|
132
183
|
await sendEvent(apiUrl, apiKey, {
|
|
133
|
-
...BASE_FIELDS(sessionId, "search", path),
|
|
184
|
+
...BASE_FIELDS(sessionId, "search", path, `Search: ${query.slice(0, 60)}`, "interaction"),
|
|
134
185
|
search_query: query
|
|
135
186
|
});
|
|
136
187
|
}
|
|
137
188
|
async function trackException(apiUrl, apiKey, sessionId, path, fields) {
|
|
189
|
+
const eventName = fields.exception_type ? `${fields.exception_type}: ${fields.exception_message.slice(0, 60)}` : fields.exception_message.slice(0, 80);
|
|
138
190
|
await sendEvent(apiUrl, apiKey, {
|
|
139
|
-
...BASE_FIELDS(sessionId, "js_exception", path),
|
|
191
|
+
...BASE_FIELDS(sessionId, "js_exception", path, eventName, "error"),
|
|
140
192
|
...fields
|
|
141
193
|
});
|
|
142
194
|
}
|
|
@@ -148,12 +200,29 @@ async function trackError(apiUrl, apiKey, sessionId, path, error) {
|
|
|
148
200
|
});
|
|
149
201
|
}
|
|
150
202
|
async function trackCTA(apiUrl, apiKey, sessionId, path, ctaId, ctaVariant) {
|
|
203
|
+
const eventName = ctaVariant ? `${ctaId} (${ctaVariant})` : ctaId;
|
|
151
204
|
await sendEvent(apiUrl, apiKey, {
|
|
152
|
-
...BASE_FIELDS(sessionId, "cta_click", path),
|
|
205
|
+
...BASE_FIELDS(sessionId, "cta_click", path, eventName, "conversion"),
|
|
153
206
|
cta_id: ctaId,
|
|
154
207
|
cta_variant: ctaVariant || null
|
|
155
208
|
});
|
|
156
209
|
}
|
|
210
|
+
var WEB_VITAL_FIELDS = {
|
|
211
|
+
lcp_ms: "lcp_ms",
|
|
212
|
+
cls_score: "cls_score",
|
|
213
|
+
inp_ms: "inp_ms",
|
|
214
|
+
ttfb_ms: "ttfb_ms",
|
|
215
|
+
fcp_ms: "fcp_ms"
|
|
216
|
+
};
|
|
217
|
+
async function trackWebVital(apiUrl, apiKey, sessionId, path, metric) {
|
|
218
|
+
const field = WEB_VITAL_FIELDS[metric.name];
|
|
219
|
+
if (!field) return;
|
|
220
|
+
await sendEvent(apiUrl, apiKey, {
|
|
221
|
+
...BASE_FIELDS(sessionId, "web_vital", path, metric.name.toUpperCase(), "performance"),
|
|
222
|
+
[field]: metric.value,
|
|
223
|
+
rating: metric.rating
|
|
224
|
+
});
|
|
225
|
+
}
|
|
157
226
|
|
|
158
227
|
// src/lib/presence.ts
|
|
159
228
|
function connectPresence(apiKey, sessionId, wsUrl) {
|
|
@@ -238,5 +307,6 @@ export {
|
|
|
238
307
|
trackException,
|
|
239
308
|
trackPageview,
|
|
240
309
|
trackScroll,
|
|
241
|
-
trackSearch
|
|
310
|
+
trackSearch,
|
|
311
|
+
trackWebVital
|
|
242
312
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arthurreira/analytics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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
|
}
|