@arthurreira/analytics 0.21.0 → 0.23.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 +15 -2
- package/dist/client.js +48 -15
- package/dist/index.js +28 -12
- package/package.json +1 -1
package/dist/af-analytics.umd.js
CHANGED
|
@@ -133,6 +133,16 @@ var AfAnalytics = (() => {
|
|
|
133
133
|
}).catch(() => {
|
|
134
134
|
});
|
|
135
135
|
}
|
|
136
|
+
function sendHeartbeat(apiUrl, apiKey, sessionId) {
|
|
137
|
+
if (isOptedOut()) return;
|
|
138
|
+
fetch(`${apiUrl}/sessions/${sessionId}/heartbeat`, {
|
|
139
|
+
method: "PATCH",
|
|
140
|
+
headers: { "Content-Type": "application/json" },
|
|
141
|
+
body: JSON.stringify({ api_key: apiKey }),
|
|
142
|
+
keepalive: true
|
|
143
|
+
}).catch(() => {
|
|
144
|
+
});
|
|
145
|
+
}
|
|
136
146
|
function baseFields(sessionId, eventType) {
|
|
137
147
|
return {
|
|
138
148
|
session_id: sessionId,
|
|
@@ -293,7 +303,10 @@ var AfAnalytics = (() => {
|
|
|
293
303
|
function startSession(id) {
|
|
294
304
|
sessionId = id;
|
|
295
305
|
closeResponder = openSessionResponder(id);
|
|
296
|
-
idleTimer = setInterval(
|
|
306
|
+
idleTimer = setInterval(() => {
|
|
307
|
+
touchSession();
|
|
308
|
+
sendHeartbeat(apiUrl, apiKey, id);
|
|
309
|
+
}, IDLE_TOUCH_INTERVAL_MS);
|
|
297
310
|
pending.splice(0).forEach((fn) => fn(id));
|
|
298
311
|
}
|
|
299
312
|
function endSession() {
|
|
@@ -304,7 +317,7 @@ var AfAnalytics = (() => {
|
|
|
304
317
|
idleTimer = null;
|
|
305
318
|
}
|
|
306
319
|
closeResponder();
|
|
307
|
-
navigator.sendBeacon(`${apiUrl}/sessions/${sessionId}/end
|
|
320
|
+
navigator.sendBeacon(`${apiUrl}/sessions/${sessionId}/end`, JSON.stringify({ api_key: apiKey }));
|
|
308
321
|
clearSession();
|
|
309
322
|
}
|
|
310
323
|
function restartSession() {
|
package/dist/client.js
CHANGED
|
@@ -41,6 +41,12 @@ function getConnectionType() {
|
|
|
41
41
|
const nav = navigator;
|
|
42
42
|
return nav.connection?.effectiveType ?? null;
|
|
43
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
|
+
}
|
|
44
50
|
function resolveElementHref(element) {
|
|
45
51
|
if (element.tagName.toLowerCase() === "a") {
|
|
46
52
|
return element.href || null;
|
|
@@ -48,6 +54,15 @@ function resolveElementHref(element) {
|
|
|
48
54
|
const anchor = element.closest("a");
|
|
49
55
|
return anchor ? anchor.href || null : null;
|
|
50
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
|
+
}
|
|
51
66
|
function isExternalHref(href) {
|
|
52
67
|
if (!href) return null;
|
|
53
68
|
if (typeof window === "undefined") return null;
|
|
@@ -111,6 +126,17 @@ async function createSession(apiUrl, apiKey, visitorId) {
|
|
|
111
126
|
return null;
|
|
112
127
|
}
|
|
113
128
|
}
|
|
129
|
+
async function sendHeartbeat(apiUrl, apiKey, sessionId) {
|
|
130
|
+
if (isOptedOut()) return;
|
|
131
|
+
try {
|
|
132
|
+
await fetch(`${apiUrl}/sessions/${sessionId}/heartbeat`, {
|
|
133
|
+
method: "PATCH",
|
|
134
|
+
headers: { "Content-Type": "application/json" },
|
|
135
|
+
body: JSON.stringify({ api_key: apiKey })
|
|
136
|
+
});
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
}
|
|
114
140
|
async function sendEvent(apiUrl, apiKey, payload) {
|
|
115
141
|
if (isOptedOut()) return;
|
|
116
142
|
await fetch(`${apiUrl}/events`, {
|
|
@@ -135,23 +161,24 @@ async function trackPageview(apiUrl, apiKey, sessionId, path, routeName) {
|
|
|
135
161
|
});
|
|
136
162
|
}
|
|
137
163
|
async function trackClick(apiUrl, apiKey, sessionId, path, e, element) {
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
const
|
|
164
|
+
const target = resolveInteractiveTarget(element);
|
|
165
|
+
const href = resolveElementHref(target);
|
|
166
|
+
const ariaLabel = target.getAttribute("aria-label");
|
|
167
|
+
const rawText = target.innerText ?? target.textContent ?? void 0;
|
|
168
|
+
const eventName = cleanLabel(ariaLabel, 60) ?? cleanLabel(rawText, 60) ?? target.tagName.toLowerCase();
|
|
142
169
|
await sendEvent(apiUrl, apiKey, {
|
|
143
170
|
...BASE_FIELDS(sessionId, "click", path, eventName, "interaction"),
|
|
144
171
|
x_position: e.clientX,
|
|
145
172
|
y_position: e.clientY,
|
|
146
|
-
element_id:
|
|
147
|
-
element_class:
|
|
148
|
-
element_text:
|
|
149
|
-
tag_name:
|
|
173
|
+
element_id: target.id || null,
|
|
174
|
+
element_class: safeClassName(target),
|
|
175
|
+
element_text: cleanLabel(rawText, 100),
|
|
176
|
+
tag_name: target.tagName.toLowerCase(),
|
|
150
177
|
element_href: href,
|
|
151
|
-
element_role:
|
|
152
|
-
element_aria_label:
|
|
153
|
-
element_type:
|
|
154
|
-
element_name:
|
|
178
|
+
element_role: target.getAttribute("role") || null,
|
|
179
|
+
element_aria_label: ariaLabel || null,
|
|
180
|
+
element_type: target.type || null,
|
|
181
|
+
element_name: target.name || null,
|
|
155
182
|
is_external_link: isExternalHref(href),
|
|
156
183
|
modifier_keys: {
|
|
157
184
|
alt: e.altKey,
|
|
@@ -400,6 +427,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl, routeName) {
|
|
|
400
427
|
idleTimerRef.current = setInterval(() => {
|
|
401
428
|
if (!hasSentEnd.current) {
|
|
402
429
|
localStorage.setItem("af_session_last_activity", String(Date.now()));
|
|
430
|
+
sendHeartbeat(apiUrl, apiKey, id);
|
|
403
431
|
}
|
|
404
432
|
}, IDLE_TOUCH_INTERVAL_MS);
|
|
405
433
|
if (wsUrl) {
|
|
@@ -424,7 +452,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl, routeName) {
|
|
|
424
452
|
closeResponderRef.current();
|
|
425
453
|
presenceRef.current?.disconnect();
|
|
426
454
|
presenceRef.current = null;
|
|
427
|
-
navigator.sendBeacon(`${apiUrl}/sessions/${sessionId.current}/end
|
|
455
|
+
navigator.sendBeacon(`${apiUrl}/sessions/${sessionId.current}/end`, JSON.stringify({ api_key: apiKey }));
|
|
428
456
|
localStorage.removeItem("af_session_id");
|
|
429
457
|
localStorage.removeItem("af_session_last_activity");
|
|
430
458
|
};
|
|
@@ -448,6 +476,7 @@ function useAnalytics(apiUrl, apiKey, currentPath, wsUrl, routeName) {
|
|
|
448
476
|
idleTimerRef.current = setInterval(() => {
|
|
449
477
|
if (!hasSentEnd.current) {
|
|
450
478
|
localStorage.setItem("af_session_last_activity", String(Date.now()));
|
|
479
|
+
sendHeartbeat(apiUrl, apiKey, id);
|
|
451
480
|
}
|
|
452
481
|
}, IDLE_TOUCH_INTERVAL_MS);
|
|
453
482
|
if (wsUrl) {
|
|
@@ -542,6 +571,10 @@ function Analytics({ apiKey, apiUrl, wsUrl, routeName }) {
|
|
|
542
571
|
const { trackPageview: trackPageview2, trackClick: trackClick2, trackScroll: trackScroll2, trackCopy: trackCopy2, trackException: trackException2, trackWebVital: trackWebVital2 } = useAnalytics(apiUrl, apiKey, pathname, wsUrl, routeName);
|
|
543
572
|
const lastTracked = useRef2(null);
|
|
544
573
|
const lastScrollDepth = useRef2(0);
|
|
574
|
+
const trackWebVitalRef = useRef2(trackWebVital2);
|
|
575
|
+
useEffect2(() => {
|
|
576
|
+
trackWebVitalRef.current = trackWebVital2;
|
|
577
|
+
});
|
|
545
578
|
useEffect2(() => {
|
|
546
579
|
if (lastTracked.current === pathname) return;
|
|
547
580
|
lastTracked.current = pathname;
|
|
@@ -551,14 +584,14 @@ function Analytics({ apiKey, apiUrl, wsUrl, routeName }) {
|
|
|
551
584
|
useEffect2(() => {
|
|
552
585
|
if (typeof window === "undefined") return;
|
|
553
586
|
const report = (name, value, rating) => {
|
|
554
|
-
|
|
587
|
+
trackWebVitalRef.current({ name, value, rating });
|
|
555
588
|
};
|
|
556
589
|
onLCP((m) => report("lcp_ms", Math.round(m.value), m.rating));
|
|
557
590
|
onCLS((m) => report("cls_score", m.value, m.rating));
|
|
558
591
|
onINP((m) => report("inp_ms", Math.round(m.value), m.rating));
|
|
559
592
|
onTTFB((m) => report("ttfb_ms", Math.round(m.value), m.rating));
|
|
560
593
|
onFCP((m) => report("fcp_ms", Math.round(m.value), m.rating));
|
|
561
|
-
}, [
|
|
594
|
+
}, []);
|
|
562
595
|
useEffect2(() => {
|
|
563
596
|
if (typeof window === "undefined") return;
|
|
564
597
|
const handler = (e) => trackClick2(e, e.target);
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,12 @@ function getConnectionType() {
|
|
|
42
42
|
const nav = navigator;
|
|
43
43
|
return nav.connection?.effectiveType ?? null;
|
|
44
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
|
+
}
|
|
45
51
|
function resolveElementHref(element) {
|
|
46
52
|
if (element.tagName.toLowerCase() === "a") {
|
|
47
53
|
return element.href || null;
|
|
@@ -49,6 +55,15 @@ function resolveElementHref(element) {
|
|
|
49
55
|
const anchor = element.closest("a");
|
|
50
56
|
return anchor ? anchor.href || null : null;
|
|
51
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
|
+
}
|
|
52
67
|
function isExternalHref(href) {
|
|
53
68
|
if (!href) return null;
|
|
54
69
|
if (typeof window === "undefined") return null;
|
|
@@ -136,23 +151,24 @@ async function trackPageview(apiUrl, apiKey, sessionId, path, routeName) {
|
|
|
136
151
|
});
|
|
137
152
|
}
|
|
138
153
|
async function trackClick(apiUrl, apiKey, sessionId, path, e, element) {
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
const
|
|
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();
|
|
143
159
|
await sendEvent(apiUrl, apiKey, {
|
|
144
160
|
...BASE_FIELDS(sessionId, "click", path, eventName, "interaction"),
|
|
145
161
|
x_position: e.clientX,
|
|
146
162
|
y_position: e.clientY,
|
|
147
|
-
element_id:
|
|
148
|
-
element_class:
|
|
149
|
-
element_text:
|
|
150
|
-
tag_name:
|
|
163
|
+
element_id: target.id || null,
|
|
164
|
+
element_class: safeClassName(target),
|
|
165
|
+
element_text: cleanLabel(rawText, 100),
|
|
166
|
+
tag_name: target.tagName.toLowerCase(),
|
|
151
167
|
element_href: href,
|
|
152
|
-
element_role:
|
|
153
|
-
element_aria_label:
|
|
154
|
-
element_type:
|
|
155
|
-
element_name:
|
|
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,
|
|
156
172
|
is_external_link: isExternalHref(href),
|
|
157
173
|
modifier_keys: {
|
|
158
174
|
alt: e.altKey,
|