@arthurreira/analytics 0.16.1 → 0.17.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 +34 -10
- package/dist/client.d.ts +3 -2
- package/dist/client.js +106 -22
- package/dist/index.js +18 -2
- package/package.json +1 -1
package/dist/af-analytics.umd.js
CHANGED
|
@@ -107,7 +107,7 @@ var AfAnalytics = (() => {
|
|
|
107
107
|
session_id: sessionId,
|
|
108
108
|
event_type: eventType,
|
|
109
109
|
path: window.location.pathname,
|
|
110
|
-
page_url: window.location.
|
|
110
|
+
page_url: `${window.location.origin}${window.location.pathname}`
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
113
|
async function createRemoteSession(apiUrl, apiKey, visitorId) {
|
|
@@ -136,20 +136,13 @@ var AfAnalytics = (() => {
|
|
|
136
136
|
utm_term: params.get("utm_term"),
|
|
137
137
|
utm_content: params.get("utm_content")
|
|
138
138
|
};
|
|
139
|
-
console.log(`[AF Analytics] Creating session at ${apiUrl}/sessions with API key: ${apiKey.slice(0, 10)}...`);
|
|
140
139
|
const res = await fetch(`${apiUrl}/sessions`, {
|
|
141
140
|
method: "POST",
|
|
142
141
|
headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
|
|
143
142
|
body: JSON.stringify(body)
|
|
144
143
|
});
|
|
145
|
-
|
|
146
|
-
if (!res.ok) {
|
|
147
|
-
const errText = await res.text();
|
|
148
|
-
console.error(`[AF Analytics] Session creation failed: ${errText}`);
|
|
149
|
-
return "";
|
|
150
|
-
}
|
|
144
|
+
if (!res.ok) return "";
|
|
151
145
|
const data = await res.json();
|
|
152
|
-
console.log(`[AF Analytics] Session created: ${data.id}`);
|
|
153
146
|
return data.id;
|
|
154
147
|
}
|
|
155
148
|
async function getOrCreateSession(apiUrl, apiKey) {
|
|
@@ -209,6 +202,28 @@ var AfAnalytics = (() => {
|
|
|
209
202
|
}).catch(() => {
|
|
210
203
|
});
|
|
211
204
|
enqueue((id) => trackPageview(apiUrl, apiKey, id));
|
|
205
|
+
let lastPath = window.location.pathname;
|
|
206
|
+
const origPushState = history.pushState.bind(history);
|
|
207
|
+
history.pushState = (...args) => {
|
|
208
|
+
origPushState(...args);
|
|
209
|
+
const newPath = window.location.pathname;
|
|
210
|
+
if (newPath !== lastPath) {
|
|
211
|
+
lastPath = newPath;
|
|
212
|
+
lastScrollDepth = 0;
|
|
213
|
+
enqueue((id) => trackPageview(apiUrl, apiKey, id));
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
window.addEventListener("popstate", () => {
|
|
217
|
+
const newPath = window.location.pathname;
|
|
218
|
+
if (newPath !== lastPath) {
|
|
219
|
+
lastPath = newPath;
|
|
220
|
+
lastScrollDepth = 0;
|
|
221
|
+
enqueue((id) => trackPageview(apiUrl, apiKey, id));
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
window.addEventListener("hashchange", () => {
|
|
225
|
+
enqueue((id) => trackPageview(apiUrl, apiKey, id));
|
|
226
|
+
});
|
|
212
227
|
window.addEventListener("click", (e) => {
|
|
213
228
|
enqueue((id) => trackClick(apiUrl, apiKey, id, e));
|
|
214
229
|
});
|
|
@@ -254,9 +269,18 @@ var AfAnalytics = (() => {
|
|
|
254
269
|
navigator.sendBeacon(`${apiUrl}/sessions/${sessionId}/end`);
|
|
255
270
|
clearSession();
|
|
256
271
|
};
|
|
272
|
+
let visHideTimer = null;
|
|
257
273
|
document.addEventListener("visibilitychange", () => {
|
|
258
|
-
if (document.visibilityState === "hidden")
|
|
274
|
+
if (document.visibilityState === "hidden") {
|
|
275
|
+
visHideTimer = setTimeout(endSession, 5e3);
|
|
276
|
+
} else {
|
|
277
|
+
if (visHideTimer !== null) {
|
|
278
|
+
clearTimeout(visHideTimer);
|
|
279
|
+
visHideTimer = null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
259
282
|
});
|
|
283
|
+
window.addEventListener("pagehide", endSession);
|
|
260
284
|
window.addEventListener("beforeunload", endSession);
|
|
261
285
|
}
|
|
262
286
|
if (document.readyState === "loading") {
|
package/dist/client.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
interface AnalyticsProps {
|
|
2
2
|
apiKey: string;
|
|
3
3
|
apiUrl: string;
|
|
4
|
+
wsUrl?: string;
|
|
4
5
|
}
|
|
5
|
-
declare function Analytics({ apiKey, apiUrl }: AnalyticsProps): null;
|
|
6
|
+
declare function Analytics({ apiKey, apiUrl, wsUrl }: AnalyticsProps): null;
|
|
6
7
|
|
|
7
8
|
interface ExceptionFields {
|
|
8
9
|
exception_type: string;
|
|
@@ -10,7 +11,7 @@ interface ExceptionFields {
|
|
|
10
11
|
stack_trace: string | null;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
declare function useAnalytics(apiUrl: string, apiKey: string): {
|
|
14
|
+
declare function useAnalytics(apiUrl: string, apiKey: string, currentPath: string, wsUrl?: string): {
|
|
14
15
|
trackPageview: (path: string) => void;
|
|
15
16
|
trackClick: (e: MouseEvent, element: HTMLElement) => void;
|
|
16
17
|
trackScroll: (depth: number) => void;
|
package/dist/client.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
// src/components/Analytics.tsx
|
|
5
5
|
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
6
|
+
import { usePathname } from "next/navigation";
|
|
6
7
|
|
|
7
8
|
// src/hooks/useAnalytics.ts
|
|
8
9
|
import { useEffect, useRef } from "react";
|
|
@@ -12,7 +13,7 @@ var BASE_FIELDS = (sessionId, eventType, path) => ({
|
|
|
12
13
|
session_id: sessionId,
|
|
13
14
|
event_type: eventType,
|
|
14
15
|
path,
|
|
15
|
-
page_url: typeof window !== "undefined" ? window.location.
|
|
16
|
+
page_url: typeof window !== "undefined" ? `${window.location.origin}${path}` : ""
|
|
16
17
|
});
|
|
17
18
|
function getGpu() {
|
|
18
19
|
try {
|
|
@@ -88,7 +89,6 @@ async function sendEvent(apiUrl, apiKey, payload) {
|
|
|
88
89
|
async function trackPageview(apiUrl, apiKey, sessionId, path) {
|
|
89
90
|
await sendEvent(apiUrl, apiKey, {
|
|
90
91
|
...BASE_FIELDS(sessionId, "pageview", path),
|
|
91
|
-
page_url: `${typeof window !== "undefined" ? window.location.origin : ""}${path}`,
|
|
92
92
|
page_title: typeof document !== "undefined" ? document.title || null : null,
|
|
93
93
|
referrer: typeof document !== "undefined" ? document.referrer || null : null,
|
|
94
94
|
scroll_depth: 0
|
|
@@ -143,6 +143,77 @@ async function trackCTA(apiUrl, apiKey, sessionId, path, ctaId, ctaVariant) {
|
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// src/lib/presence.ts
|
|
147
|
+
var DEFAULT_WS_URL = "wss://edge.arthurreira.dev/realtime";
|
|
148
|
+
function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
|
|
149
|
+
if (typeof WebSocket === "undefined") {
|
|
150
|
+
return { disconnect: () => {
|
|
151
|
+
} };
|
|
152
|
+
}
|
|
153
|
+
let ws = null;
|
|
154
|
+
let attempts = 0;
|
|
155
|
+
let intentionalClose = false;
|
|
156
|
+
let retryTimer = null;
|
|
157
|
+
let pingInterval = null;
|
|
158
|
+
let targetUrl;
|
|
159
|
+
try {
|
|
160
|
+
const u = new URL(wsUrl);
|
|
161
|
+
u.searchParams.set("api_key", apiKey);
|
|
162
|
+
u.searchParams.set("session_id", sessionId);
|
|
163
|
+
targetUrl = u.toString();
|
|
164
|
+
} catch {
|
|
165
|
+
return { disconnect: () => {
|
|
166
|
+
} };
|
|
167
|
+
}
|
|
168
|
+
function clearPing() {
|
|
169
|
+
if (pingInterval !== null) {
|
|
170
|
+
clearInterval(pingInterval);
|
|
171
|
+
pingInterval = null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function connect() {
|
|
175
|
+
try {
|
|
176
|
+
ws = new WebSocket(targetUrl);
|
|
177
|
+
} catch {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
ws.onopen = () => {
|
|
181
|
+
attempts = 0;
|
|
182
|
+
ws.send(JSON.stringify({ type: "join", api_key: apiKey, session_id: sessionId }));
|
|
183
|
+
pingInterval = setInterval(() => {
|
|
184
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
185
|
+
ws.send(JSON.stringify({ type: "ping", session_id: sessionId }));
|
|
186
|
+
}
|
|
187
|
+
}, 3e4);
|
|
188
|
+
};
|
|
189
|
+
ws.onclose = () => {
|
|
190
|
+
clearPing();
|
|
191
|
+
if (intentionalClose || attempts >= 3) return;
|
|
192
|
+
const delay = Math.pow(2, attempts) * 1e3;
|
|
193
|
+
attempts++;
|
|
194
|
+
retryTimer = setTimeout(connect, delay);
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
connect();
|
|
198
|
+
return {
|
|
199
|
+
disconnect: () => {
|
|
200
|
+
intentionalClose = true;
|
|
201
|
+
clearPing();
|
|
202
|
+
if (retryTimer !== null) {
|
|
203
|
+
clearTimeout(retryTimer);
|
|
204
|
+
retryTimer = null;
|
|
205
|
+
}
|
|
206
|
+
if (ws) {
|
|
207
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
208
|
+
ws.send(JSON.stringify({ type: "leave", session_id: sessionId }));
|
|
209
|
+
}
|
|
210
|
+
ws.close();
|
|
211
|
+
ws = null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
146
217
|
// src/hooks/useAnalytics.ts
|
|
147
218
|
var SESSION_EXPIRY_MINUTES = 30;
|
|
148
219
|
var _sessionFlight = null;
|
|
@@ -173,11 +244,11 @@ async function getOrCreateSession(apiUrl, apiKey, visitorId) {
|
|
|
173
244
|
});
|
|
174
245
|
return _sessionFlight;
|
|
175
246
|
}
|
|
176
|
-
function useAnalytics(apiUrl, apiKey) {
|
|
247
|
+
function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
177
248
|
const sessionId = useRef(null);
|
|
178
249
|
const pendingEvents = useRef([]);
|
|
179
|
-
const pathname = useRef(typeof window !== "undefined" ? window?.location?.pathname ?? "" : "");
|
|
180
250
|
const hasSentEnd = useRef(false);
|
|
251
|
+
const presenceRef = useRef(null);
|
|
181
252
|
useEffect(() => {
|
|
182
253
|
if (typeof window === "undefined") return;
|
|
183
254
|
const visitor_id = localStorage.getItem("af_analytics_visitor_id") || crypto.randomUUID();
|
|
@@ -186,6 +257,9 @@ function useAnalytics(apiUrl, apiKey) {
|
|
|
186
257
|
getOrCreateSession(apiUrl, apiKey, visitor_id).then((id) => {
|
|
187
258
|
if (cancelled || !id) return;
|
|
188
259
|
sessionId.current = id;
|
|
260
|
+
if (wsUrl) {
|
|
261
|
+
presenceRef.current = connectPresence(apiKey, id, wsUrl);
|
|
262
|
+
}
|
|
189
263
|
pendingEvents.current.forEach((fn) => fn());
|
|
190
264
|
pendingEvents.current = [];
|
|
191
265
|
});
|
|
@@ -198,19 +272,30 @@ function useAnalytics(apiUrl, apiKey) {
|
|
|
198
272
|
const sendEnd = () => {
|
|
199
273
|
if (!sessionId.current || hasSentEnd.current) return;
|
|
200
274
|
hasSentEnd.current = true;
|
|
275
|
+
presenceRef.current?.disconnect();
|
|
201
276
|
navigator.sendBeacon(`${apiUrl}/sessions/${sessionId.current}/end`);
|
|
202
277
|
localStorage.removeItem("af_session_id");
|
|
203
278
|
localStorage.removeItem("af_session_last_activity");
|
|
204
279
|
};
|
|
280
|
+
let visHideTimer = null;
|
|
205
281
|
const handleVisibility = () => {
|
|
206
|
-
if (document.visibilityState === "hidden")
|
|
282
|
+
if (document.visibilityState === "hidden") {
|
|
283
|
+
visHideTimer = setTimeout(sendEnd, 5e3);
|
|
284
|
+
} else {
|
|
285
|
+
if (visHideTimer !== null) {
|
|
286
|
+
clearTimeout(visHideTimer);
|
|
287
|
+
visHideTimer = null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
207
290
|
};
|
|
208
|
-
const handleUnload = () => sendEnd();
|
|
209
291
|
document.addEventListener("visibilitychange", handleVisibility);
|
|
210
|
-
window.addEventListener("
|
|
292
|
+
window.addEventListener("pagehide", sendEnd);
|
|
293
|
+
window.addEventListener("beforeunload", sendEnd);
|
|
211
294
|
return () => {
|
|
295
|
+
if (visHideTimer !== null) clearTimeout(visHideTimer);
|
|
212
296
|
document.removeEventListener("visibilitychange", handleVisibility);
|
|
213
|
-
window.removeEventListener("
|
|
297
|
+
window.removeEventListener("pagehide", sendEnd);
|
|
298
|
+
window.removeEventListener("beforeunload", sendEnd);
|
|
214
299
|
};
|
|
215
300
|
}, [apiUrl]);
|
|
216
301
|
function enqueueOrRun(fn) {
|
|
@@ -226,38 +311,39 @@ function useAnalytics(apiUrl, apiKey) {
|
|
|
226
311
|
enqueueOrRun(() => trackPageview(apiUrl, apiKey, sessionId.current, path));
|
|
227
312
|
},
|
|
228
313
|
trackClick: (e, element) => {
|
|
229
|
-
enqueueOrRun(() => trackClick(apiUrl, apiKey, sessionId.current,
|
|
314
|
+
enqueueOrRun(() => trackClick(apiUrl, apiKey, sessionId.current, currentPath, e, element));
|
|
230
315
|
},
|
|
231
316
|
trackScroll: (depth) => {
|
|
232
|
-
enqueueOrRun(() => trackScroll(apiUrl, apiKey, sessionId.current,
|
|
317
|
+
enqueueOrRun(() => trackScroll(apiUrl, apiKey, sessionId.current, currentPath, depth));
|
|
233
318
|
},
|
|
234
319
|
trackCopy: () => {
|
|
235
|
-
enqueueOrRun(() => trackCopy(apiUrl, apiKey, sessionId.current,
|
|
320
|
+
enqueueOrRun(() => trackCopy(apiUrl, apiKey, sessionId.current, currentPath));
|
|
236
321
|
},
|
|
237
322
|
trackException: (fields) => {
|
|
238
|
-
enqueueOrRun(() => trackException(apiUrl, apiKey, sessionId.current,
|
|
323
|
+
enqueueOrRun(() => trackException(apiUrl, apiKey, sessionId.current, currentPath, fields));
|
|
239
324
|
},
|
|
240
325
|
trackError: (error) => {
|
|
241
|
-
enqueueOrRun(() => trackError(apiUrl, apiKey, sessionId.current,
|
|
326
|
+
enqueueOrRun(() => trackError(apiUrl, apiKey, sessionId.current, currentPath, error));
|
|
242
327
|
},
|
|
243
328
|
trackCTA: (ctaId, ctaVariant) => {
|
|
244
|
-
enqueueOrRun(() => trackCTA(apiUrl, apiKey, sessionId.current,
|
|
329
|
+
enqueueOrRun(() => trackCTA(apiUrl, apiKey, sessionId.current, currentPath, ctaId, ctaVariant));
|
|
245
330
|
},
|
|
246
331
|
trackSearch: (query) => {
|
|
247
|
-
enqueueOrRun(() => trackSearch(apiUrl, apiKey, sessionId.current,
|
|
332
|
+
enqueueOrRun(() => trackSearch(apiUrl, apiKey, sessionId.current, currentPath, query));
|
|
248
333
|
}
|
|
249
334
|
};
|
|
250
335
|
}
|
|
251
336
|
|
|
252
337
|
// src/components/Analytics.tsx
|
|
253
|
-
function Analytics({ apiKey, apiUrl }) {
|
|
254
|
-
const
|
|
338
|
+
function Analytics({ apiKey, apiUrl, wsUrl }) {
|
|
339
|
+
const pathname = usePathname();
|
|
340
|
+
const { trackPageview: trackPageview2, trackClick: trackClick2, trackScroll: trackScroll2, trackCopy: trackCopy2, trackException: trackException2 } = useAnalytics(apiUrl, apiKey, pathname, wsUrl);
|
|
255
341
|
const lastTracked = useRef2(null);
|
|
256
342
|
const lastScrollDepth = useRef2(0);
|
|
257
|
-
const pathname = typeof window !== "undefined" ? window.location.pathname : "";
|
|
258
343
|
useEffect2(() => {
|
|
259
344
|
if (lastTracked.current === pathname) return;
|
|
260
345
|
lastTracked.current = pathname;
|
|
346
|
+
lastScrollDepth.current = 0;
|
|
261
347
|
trackPageview2(pathname);
|
|
262
348
|
}, [pathname, trackPageview2]);
|
|
263
349
|
useEffect2(() => {
|
|
@@ -269,12 +355,10 @@ function Analytics({ apiKey, apiUrl }) {
|
|
|
269
355
|
useEffect2(() => {
|
|
270
356
|
if (typeof window === "undefined") return;
|
|
271
357
|
const handler = () => {
|
|
272
|
-
const scrolled = window.scrollY;
|
|
273
358
|
const total = document.documentElement.scrollHeight - window.innerHeight;
|
|
274
359
|
if (total <= 0) return;
|
|
275
|
-
const depth = Math.round(
|
|
276
|
-
const
|
|
277
|
-
for (const m of milestones) {
|
|
360
|
+
const depth = Math.round(window.scrollY / total * 100);
|
|
361
|
+
for (const m of [25, 50, 75, 100]) {
|
|
278
362
|
if (depth >= m && lastScrollDepth.current < m) {
|
|
279
363
|
lastScrollDepth.current = m;
|
|
280
364
|
trackScroll2(m);
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ var BASE_FIELDS = (sessionId, eventType, path) => ({
|
|
|
3
3
|
session_id: sessionId,
|
|
4
4
|
event_type: eventType,
|
|
5
5
|
path,
|
|
6
|
-
page_url: typeof window !== "undefined" ? window.location.
|
|
6
|
+
page_url: typeof window !== "undefined" ? `${window.location.origin}${path}` : ""
|
|
7
7
|
});
|
|
8
8
|
function getGpu() {
|
|
9
9
|
try {
|
|
@@ -79,7 +79,6 @@ async function sendEvent(apiUrl, apiKey, payload) {
|
|
|
79
79
|
async function trackPageview(apiUrl, apiKey, sessionId, path) {
|
|
80
80
|
await sendEvent(apiUrl, apiKey, {
|
|
81
81
|
...BASE_FIELDS(sessionId, "pageview", path),
|
|
82
|
-
page_url: `${typeof window !== "undefined" ? window.location.origin : ""}${path}`,
|
|
83
82
|
page_title: typeof document !== "undefined" ? document.title || null : null,
|
|
84
83
|
referrer: typeof document !== "undefined" ? document.referrer || null : null,
|
|
85
84
|
scroll_depth: 0
|
|
@@ -145,6 +144,7 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
|
|
|
145
144
|
let attempts = 0;
|
|
146
145
|
let intentionalClose = false;
|
|
147
146
|
let retryTimer = null;
|
|
147
|
+
let pingInterval = null;
|
|
148
148
|
let targetUrl;
|
|
149
149
|
try {
|
|
150
150
|
const u = new URL(wsUrl);
|
|
@@ -155,6 +155,12 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
|
|
|
155
155
|
return { disconnect: () => {
|
|
156
156
|
} };
|
|
157
157
|
}
|
|
158
|
+
function clearPing() {
|
|
159
|
+
if (pingInterval !== null) {
|
|
160
|
+
clearInterval(pingInterval);
|
|
161
|
+
pingInterval = null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
158
164
|
function connect() {
|
|
159
165
|
try {
|
|
160
166
|
ws = new WebSocket(targetUrl);
|
|
@@ -164,8 +170,14 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
|
|
|
164
170
|
ws.onopen = () => {
|
|
165
171
|
attempts = 0;
|
|
166
172
|
ws.send(JSON.stringify({ type: "join", api_key: apiKey, session_id: sessionId }));
|
|
173
|
+
pingInterval = setInterval(() => {
|
|
174
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
175
|
+
ws.send(JSON.stringify({ type: "ping", session_id: sessionId }));
|
|
176
|
+
}
|
|
177
|
+
}, 3e4);
|
|
167
178
|
};
|
|
168
179
|
ws.onclose = () => {
|
|
180
|
+
clearPing();
|
|
169
181
|
if (intentionalClose || attempts >= 3) return;
|
|
170
182
|
const delay = Math.pow(2, attempts) * 1e3;
|
|
171
183
|
attempts++;
|
|
@@ -176,11 +188,15 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
|
|
|
176
188
|
return {
|
|
177
189
|
disconnect: () => {
|
|
178
190
|
intentionalClose = true;
|
|
191
|
+
clearPing();
|
|
179
192
|
if (retryTimer !== null) {
|
|
180
193
|
clearTimeout(retryTimer);
|
|
181
194
|
retryTimer = null;
|
|
182
195
|
}
|
|
183
196
|
if (ws) {
|
|
197
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
198
|
+
ws.send(JSON.stringify({ type: "leave", session_id: sessionId }));
|
|
199
|
+
}
|
|
184
200
|
ws.close();
|
|
185
201
|
ws = null;
|
|
186
202
|
}
|