@arthurreira/analytics 0.19.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/af-analytics.umd.js +20 -11
- package/dist/client.d.ts +11 -2
- package/dist/client.js +122 -36
- package/dist/index.d.ts +11 -4
- package/dist/index.js +98 -30
- package/package.json +4 -1
package/dist/af-analytics.umd.js
CHANGED
|
@@ -72,12 +72,13 @@ var AfAnalytics = (() => {
|
|
|
72
72
|
var BC_CHANNEL = "af_analytics_session";
|
|
73
73
|
var BC_ADOPT_TIMEOUT_MS = 50;
|
|
74
74
|
function readConfig() {
|
|
75
|
-
var _a, _b, _c, _d;
|
|
75
|
+
var _a, _b, _c, _d, _e, _f;
|
|
76
76
|
const script = _currentScript != null ? _currentScript : document.querySelector("script[data-api-key]");
|
|
77
77
|
const apiKey = (_b = (_a = script == null ? void 0 : script.dataset.apiKey) == null ? void 0 : _a.trim()) != null ? _b : "";
|
|
78
78
|
const apiUrl = ((_d = (_c = script == null ? void 0 : script.dataset.url) == null ? void 0 : _c.trim()) != null ? _d : "").replace(/\/$/, "");
|
|
79
79
|
if (!apiKey || !apiUrl) return null;
|
|
80
|
-
|
|
80
|
+
const wsUrl = (_f = (_e = script == null ? void 0 : script.dataset.wsUrl) == null ? void 0 : _e.trim()) != null ? _f : apiUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://") + "/realtime";
|
|
81
|
+
return { apiKey, apiUrl, wsUrl };
|
|
81
82
|
}
|
|
82
83
|
function generateId() {
|
|
83
84
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
@@ -184,7 +185,7 @@ var AfAnalytics = (() => {
|
|
|
184
185
|
return () => channel.close();
|
|
185
186
|
}
|
|
186
187
|
async function createRemoteSession(apiUrl, apiKey, visitorId) {
|
|
187
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
|
|
188
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o;
|
|
188
189
|
const nav = navigator;
|
|
189
190
|
const conn = (_c = (_b = (_a = nav["connection"]) != null ? _a : nav["mozConnection"]) != null ? _b : nav["webkitConnection"]) != null ? _c : null;
|
|
190
191
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -209,14 +210,22 @@ var AfAnalytics = (() => {
|
|
|
209
210
|
utm_term: params.get("utm_term"),
|
|
210
211
|
utm_content: params.get("utm_content")
|
|
211
212
|
};
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
213
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
214
|
+
try {
|
|
215
|
+
const res = await fetch(`${apiUrl}/sessions`, {
|
|
216
|
+
method: "POST",
|
|
217
|
+
headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
|
|
218
|
+
body: JSON.stringify(body)
|
|
219
|
+
});
|
|
220
|
+
if (res.ok) {
|
|
221
|
+
const data = await res.json();
|
|
222
|
+
return (_o = (_n = data.session_id) != null ? _n : data.id) != null ? _o : "";
|
|
223
|
+
}
|
|
224
|
+
} catch (e) {
|
|
225
|
+
}
|
|
226
|
+
if (attempt < 2) await new Promise((r) => setTimeout(r, 500 * 2 ** attempt));
|
|
227
|
+
}
|
|
228
|
+
return "";
|
|
220
229
|
}
|
|
221
230
|
async function getOrCreateSession(apiUrl, apiKey) {
|
|
222
231
|
const stored = getStoredSession();
|
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,16 +199,32 @@ 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
|
-
|
|
158
|
-
function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
|
|
227
|
+
function connectPresence(apiKey, sessionId, wsUrl) {
|
|
159
228
|
if (typeof WebSocket === "undefined") {
|
|
160
229
|
return { disconnect: () => {
|
|
161
230
|
} };
|
|
@@ -307,7 +376,7 @@ async function getOrCreateSession(apiUrl, apiKey, visitorId) {
|
|
|
307
376
|
});
|
|
308
377
|
return _sessionFlight;
|
|
309
378
|
}
|
|
310
|
-
function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
379
|
+
function useAnalytics(apiUrl, apiKey, currentPath, wsUrl, routeName) {
|
|
311
380
|
const sessionId = useRef(null);
|
|
312
381
|
const pendingEvents = useRef([]);
|
|
313
382
|
const hasSentEnd = useRef(false);
|
|
@@ -384,7 +453,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
|
384
453
|
if (wsUrl) {
|
|
385
454
|
presenceRef.current = connectPresence(apiKey, id, wsUrl);
|
|
386
455
|
}
|
|
387
|
-
trackPageview(apiUrl, apiKey, id, currentPathRef.current);
|
|
456
|
+
trackPageview(apiUrl, apiKey, id, currentPathRef.current, routeName ?? null);
|
|
388
457
|
});
|
|
389
458
|
};
|
|
390
459
|
const handleStorage = (e) => {
|
|
@@ -400,7 +469,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
|
400
469
|
if (hasSentEnd.current || !sessionId.current) {
|
|
401
470
|
restartSession();
|
|
402
471
|
} else {
|
|
403
|
-
trackPageview(apiUrl, apiKey, sessionId.current, currentPathRef.current);
|
|
472
|
+
trackPageview(apiUrl, apiKey, sessionId.current, currentPathRef.current, routeName ?? null);
|
|
404
473
|
}
|
|
405
474
|
};
|
|
406
475
|
let visHideTimer = null;
|
|
@@ -438,7 +507,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
|
438
507
|
}
|
|
439
508
|
return {
|
|
440
509
|
trackPageview: (path) => {
|
|
441
|
-
enqueueOrRun(() => trackPageview(apiUrl, apiKey, sessionId.current, path));
|
|
510
|
+
enqueueOrRun(() => trackPageview(apiUrl, apiKey, sessionId.current, path, routeName ?? null));
|
|
442
511
|
},
|
|
443
512
|
trackClick: (e, element) => {
|
|
444
513
|
enqueueOrRun(() => trackClick(apiUrl, apiKey, sessionId.current, currentPath, e, element));
|
|
@@ -460,22 +529,36 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
|
460
529
|
},
|
|
461
530
|
trackSearch: (query) => {
|
|
462
531
|
enqueueOrRun(() => trackSearch(apiUrl, apiKey, sessionId.current, currentPath, query));
|
|
532
|
+
},
|
|
533
|
+
trackWebVital: (metric) => {
|
|
534
|
+
enqueueOrRun(() => trackWebVital(apiUrl, apiKey, sessionId.current, currentPath, metric));
|
|
463
535
|
}
|
|
464
536
|
};
|
|
465
537
|
}
|
|
466
538
|
|
|
467
539
|
// src/components/Analytics.tsx
|
|
468
|
-
function Analytics({ apiKey, apiUrl, wsUrl }) {
|
|
540
|
+
function Analytics({ apiKey, apiUrl, wsUrl, routeName }) {
|
|
469
541
|
const pathname = usePathname();
|
|
470
|
-
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);
|
|
471
543
|
const lastTracked = useRef2(null);
|
|
472
544
|
const lastScrollDepth = useRef2(0);
|
|
473
545
|
useEffect2(() => {
|
|
474
546
|
if (lastTracked.current === pathname) return;
|
|
475
547
|
lastTracked.current = pathname;
|
|
476
548
|
lastScrollDepth.current = 0;
|
|
477
|
-
trackPageview2(pathname);
|
|
549
|
+
setTimeout(() => trackPageview2(pathname), 0);
|
|
478
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]);
|
|
479
562
|
useEffect2(() => {
|
|
480
563
|
if (typeof window === "undefined") return;
|
|
481
564
|
const handler = (e) => trackClick2(e, e.target);
|
|
@@ -509,7 +592,10 @@ function Analytics({ apiKey, apiUrl, wsUrl }) {
|
|
|
509
592
|
const handler = (e) => trackException2({
|
|
510
593
|
exception_type: e.error?.name ?? "Error",
|
|
511
594
|
exception_message: e.message,
|
|
512
|
-
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
|
|
513
599
|
});
|
|
514
600
|
window.addEventListener("error", handler);
|
|
515
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,15 +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
|
-
declare const DEFAULT_WS_URL = "wss://edge.arthurreira.dev/realtime";
|
|
26
33
|
interface PresenceConnection {
|
|
27
34
|
disconnect: () => void;
|
|
28
35
|
}
|
|
29
|
-
declare function connectPresence(apiKey: string, sessionId: string, wsUrl
|
|
36
|
+
declare function connectPresence(apiKey: string, sessionId: string, wsUrl: string): PresenceConnection;
|
|
30
37
|
|
|
31
|
-
export {
|
|
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,16 +200,32 @@ 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
|
-
|
|
160
|
-
function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
|
|
228
|
+
function connectPresence(apiKey, sessionId, wsUrl) {
|
|
161
229
|
if (typeof WebSocket === "undefined") {
|
|
162
230
|
return { disconnect: () => {
|
|
163
231
|
} };
|
|
@@ -226,7 +294,6 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
|
|
|
226
294
|
};
|
|
227
295
|
}
|
|
228
296
|
export {
|
|
229
|
-
DEFAULT_WS_URL,
|
|
230
297
|
connectPresence,
|
|
231
298
|
consent,
|
|
232
299
|
createSession,
|
|
@@ -240,5 +307,6 @@ export {
|
|
|
240
307
|
trackException,
|
|
241
308
|
trackPageview,
|
|
242
309
|
trackScroll,
|
|
243
|
-
trackSearch
|
|
310
|
+
trackSearch,
|
|
311
|
+
trackWebVital
|
|
244
312
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arthurreira/analytics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "tsup",
|
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
"typescript": "^5.9.2",
|
|
35
35
|
"vitest": "^4.1.7"
|
|
36
36
|
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"web-vitals": "^5.1.0"
|
|
39
|
+
},
|
|
37
40
|
"peerDependencies": {
|
|
38
41
|
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
39
42
|
"react": "^18.0.0 || ^19.0.0"
|