@arthurreira/analytics 0.16.1 → 0.18.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 +143 -19
- package/dist/client.d.ts +3 -2
- package/dist/client.js +227 -23
- package/dist/index.js +18 -2
- package/package.json +1 -1
package/dist/af-analytics.umd.js
CHANGED
|
@@ -42,6 +42,9 @@ var AfAnalytics = (() => {
|
|
|
42
42
|
});
|
|
43
43
|
var _currentScript = document.currentScript;
|
|
44
44
|
var SESSION_EXPIRY_MS = 30 * 60 * 1e3;
|
|
45
|
+
var IDLE_TOUCH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
46
|
+
var BC_CHANNEL = "af_analytics_session";
|
|
47
|
+
var BC_ADOPT_TIMEOUT_MS = 50;
|
|
45
48
|
function readConfig() {
|
|
46
49
|
var _a, _b, _c, _d;
|
|
47
50
|
const script = _currentScript != null ? _currentScript : document.querySelector("script[data-api-key]");
|
|
@@ -107,9 +110,52 @@ var AfAnalytics = (() => {
|
|
|
107
110
|
session_id: sessionId,
|
|
108
111
|
event_type: eventType,
|
|
109
112
|
path: window.location.pathname,
|
|
110
|
-
page_url: window.location.
|
|
113
|
+
page_url: `${window.location.origin}${window.location.pathname}`
|
|
111
114
|
};
|
|
112
115
|
}
|
|
116
|
+
function requestSessionFromPeer() {
|
|
117
|
+
if (typeof BroadcastChannel === "undefined") return Promise.resolve(null);
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
let channel;
|
|
120
|
+
try {
|
|
121
|
+
channel = new BroadcastChannel(BC_CHANNEL);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
resolve(null);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const timer = setTimeout(() => {
|
|
127
|
+
channel.close();
|
|
128
|
+
resolve(null);
|
|
129
|
+
}, BC_ADOPT_TIMEOUT_MS);
|
|
130
|
+
channel.onmessage = (e) => {
|
|
131
|
+
var _a;
|
|
132
|
+
if (((_a = e.data) == null ? void 0 : _a.type) === "session_adopt" && typeof e.data.session_id === "string") {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
channel.close();
|
|
135
|
+
resolve(e.data.session_id);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
channel.postMessage({ type: "session_claim" });
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function openSessionResponder(sessionId) {
|
|
142
|
+
if (typeof BroadcastChannel === "undefined") return () => {
|
|
143
|
+
};
|
|
144
|
+
let channel;
|
|
145
|
+
try {
|
|
146
|
+
channel = new BroadcastChannel(BC_CHANNEL);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
return () => {
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
channel.onmessage = (e) => {
|
|
152
|
+
var _a;
|
|
153
|
+
if (((_a = e.data) == null ? void 0 : _a.type) === "session_claim") {
|
|
154
|
+
channel.postMessage({ type: "session_adopt", session_id: sessionId });
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
return () => channel.close();
|
|
158
|
+
}
|
|
113
159
|
async function createRemoteSession(apiUrl, apiKey, visitorId) {
|
|
114
160
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
|
|
115
161
|
const nav = navigator;
|
|
@@ -136,20 +182,13 @@ var AfAnalytics = (() => {
|
|
|
136
182
|
utm_term: params.get("utm_term"),
|
|
137
183
|
utm_content: params.get("utm_content")
|
|
138
184
|
};
|
|
139
|
-
console.log(`[AF Analytics] Creating session at ${apiUrl}/sessions with API key: ${apiKey.slice(0, 10)}...`);
|
|
140
185
|
const res = await fetch(`${apiUrl}/sessions`, {
|
|
141
186
|
method: "POST",
|
|
142
187
|
headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
|
|
143
188
|
body: JSON.stringify(body)
|
|
144
189
|
});
|
|
145
|
-
|
|
146
|
-
if (!res.ok) {
|
|
147
|
-
const errText = await res.text();
|
|
148
|
-
console.error(`[AF Analytics] Session creation failed: ${errText}`);
|
|
149
|
-
return "";
|
|
150
|
-
}
|
|
190
|
+
if (!res.ok) return "";
|
|
151
191
|
const data = await res.json();
|
|
152
|
-
console.log(`[AF Analytics] Session created: ${data.id}`);
|
|
153
192
|
return data.id;
|
|
154
193
|
}
|
|
155
194
|
async function getOrCreateSession(apiUrl, apiKey) {
|
|
@@ -158,9 +197,17 @@ var AfAnalytics = (() => {
|
|
|
158
197
|
touchSession();
|
|
159
198
|
return stored;
|
|
160
199
|
}
|
|
200
|
+
const adopted = await requestSessionFromPeer();
|
|
201
|
+
if (adopted) {
|
|
202
|
+
localStorage.setItem("af_session_id", adopted);
|
|
203
|
+
localStorage.setItem("af_session_last_activity", String(Date.now()));
|
|
204
|
+
return adopted;
|
|
205
|
+
}
|
|
161
206
|
const id = await createRemoteSession(apiUrl, apiKey, getVisitorId());
|
|
162
|
-
|
|
163
|
-
|
|
207
|
+
if (id) {
|
|
208
|
+
localStorage.setItem("af_session_id", id);
|
|
209
|
+
localStorage.setItem("af_session_last_activity", String(Date.now()));
|
|
210
|
+
}
|
|
164
211
|
return id;
|
|
165
212
|
}
|
|
166
213
|
function trackPageview(apiUrl, apiKey, sessionId) {
|
|
@@ -195,6 +242,9 @@ var AfAnalytics = (() => {
|
|
|
195
242
|
const pending = [];
|
|
196
243
|
let sessionEnded = false;
|
|
197
244
|
let lastScrollDepth = 0;
|
|
245
|
+
let closeResponder = () => {
|
|
246
|
+
};
|
|
247
|
+
let idleTimer = null;
|
|
198
248
|
function enqueue(fn) {
|
|
199
249
|
if (sessionId) {
|
|
200
250
|
touchSession();
|
|
@@ -203,12 +253,68 @@ var AfAnalytics = (() => {
|
|
|
203
253
|
pending.push(fn);
|
|
204
254
|
}
|
|
205
255
|
}
|
|
206
|
-
|
|
256
|
+
function startSession(id) {
|
|
207
257
|
sessionId = id;
|
|
258
|
+
closeResponder = openSessionResponder(id);
|
|
259
|
+
idleTimer = setInterval(touchSession, IDLE_TOUCH_INTERVAL_MS);
|
|
208
260
|
pending.splice(0).forEach((fn) => fn(id));
|
|
261
|
+
}
|
|
262
|
+
function endSession() {
|
|
263
|
+
if (!sessionId || sessionEnded) return;
|
|
264
|
+
sessionEnded = true;
|
|
265
|
+
if (idleTimer !== null) {
|
|
266
|
+
clearInterval(idleTimer);
|
|
267
|
+
idleTimer = null;
|
|
268
|
+
}
|
|
269
|
+
closeResponder();
|
|
270
|
+
navigator.sendBeacon(`${apiUrl}/sessions/${sessionId}/end`);
|
|
271
|
+
clearSession();
|
|
272
|
+
}
|
|
273
|
+
function restartSession() {
|
|
274
|
+
sessionEnded = false;
|
|
275
|
+
sessionId = null;
|
|
276
|
+
if (idleTimer !== null) {
|
|
277
|
+
clearInterval(idleTimer);
|
|
278
|
+
idleTimer = null;
|
|
279
|
+
}
|
|
280
|
+
closeResponder();
|
|
281
|
+
closeResponder = () => {
|
|
282
|
+
};
|
|
283
|
+
clearSession();
|
|
284
|
+
getOrCreateSession(apiUrl, apiKey).then((id) => {
|
|
285
|
+
if (!id) return;
|
|
286
|
+
startSession(id);
|
|
287
|
+
trackPageview(apiUrl, apiKey, id);
|
|
288
|
+
}).catch(() => {
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
getOrCreateSession(apiUrl, apiKey).then((id) => {
|
|
292
|
+
if (id) startSession(id);
|
|
209
293
|
}).catch(() => {
|
|
210
294
|
});
|
|
211
295
|
enqueue((id) => trackPageview(apiUrl, apiKey, id));
|
|
296
|
+
let lastPath = window.location.pathname;
|
|
297
|
+
const origPushState = history.pushState.bind(history);
|
|
298
|
+
history.pushState = (...args) => {
|
|
299
|
+
origPushState(...args);
|
|
300
|
+
const newPath = window.location.pathname;
|
|
301
|
+
if (newPath !== lastPath) {
|
|
302
|
+
lastPath = newPath;
|
|
303
|
+
lastScrollDepth = 0;
|
|
304
|
+
enqueue((id) => trackPageview(apiUrl, apiKey, id));
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
window.addEventListener("popstate", () => {
|
|
308
|
+
const newPath = window.location.pathname;
|
|
309
|
+
if (newPath !== lastPath) {
|
|
310
|
+
lastPath = newPath;
|
|
311
|
+
lastScrollDepth = 0;
|
|
312
|
+
enqueue((id) => trackPageview(apiUrl, apiKey, id));
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
window.addEventListener("hashchange", () => {
|
|
316
|
+
enqueue((id) => trackPageview(apiUrl, apiKey, id));
|
|
317
|
+
});
|
|
212
318
|
window.addEventListener("click", (e) => {
|
|
213
319
|
enqueue((id) => trackClick(apiUrl, apiKey, id, e));
|
|
214
320
|
});
|
|
@@ -248,14 +354,32 @@ var AfAnalytics = (() => {
|
|
|
248
354
|
});
|
|
249
355
|
});
|
|
250
356
|
});
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
357
|
+
window.addEventListener("storage", (e) => {
|
|
358
|
+
if (e.key === "af_session_id" && e.newValue === null && e.oldValue !== null) {
|
|
359
|
+
restartSession();
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
let visHideTimer = null;
|
|
257
363
|
document.addEventListener("visibilitychange", () => {
|
|
258
|
-
if (document.visibilityState === "hidden")
|
|
364
|
+
if (document.visibilityState === "hidden") {
|
|
365
|
+
visHideTimer = setTimeout(endSession, 5e3);
|
|
366
|
+
} else {
|
|
367
|
+
if (visHideTimer !== null) {
|
|
368
|
+
clearTimeout(visHideTimer);
|
|
369
|
+
visHideTimer = null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
window.addEventListener("pagehide", (e) => {
|
|
374
|
+
if (!e.persisted) endSession();
|
|
375
|
+
});
|
|
376
|
+
window.addEventListener("pageshow", (e) => {
|
|
377
|
+
if (!e.persisted) return;
|
|
378
|
+
if (sessionEnded || !sessionId) {
|
|
379
|
+
restartSession();
|
|
380
|
+
} else {
|
|
381
|
+
trackPageview(apiUrl, apiKey, sessionId);
|
|
382
|
+
}
|
|
259
383
|
});
|
|
260
384
|
window.addEventListener("beforeunload", endSession);
|
|
261
385
|
}
|
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,9 +143,124 @@ 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;
|
|
219
|
+
var IDLE_TOUCH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
220
|
+
var BC_CHANNEL = "af_analytics_session";
|
|
221
|
+
var BC_ADOPT_TIMEOUT_MS = 50;
|
|
148
222
|
var _sessionFlight = null;
|
|
223
|
+
function requestSessionFromPeer() {
|
|
224
|
+
if (typeof BroadcastChannel === "undefined") return Promise.resolve(null);
|
|
225
|
+
return new Promise((resolve) => {
|
|
226
|
+
let channel;
|
|
227
|
+
try {
|
|
228
|
+
channel = new BroadcastChannel(BC_CHANNEL);
|
|
229
|
+
} catch {
|
|
230
|
+
resolve(null);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const timer = setTimeout(() => {
|
|
234
|
+
channel.close();
|
|
235
|
+
resolve(null);
|
|
236
|
+
}, BC_ADOPT_TIMEOUT_MS);
|
|
237
|
+
channel.onmessage = (e) => {
|
|
238
|
+
if (e.data?.type === "session_adopt" && typeof e.data.session_id === "string") {
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
channel.close();
|
|
241
|
+
resolve(e.data.session_id);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
channel.postMessage({ type: "session_claim" });
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
function openSessionResponder(sessionId) {
|
|
248
|
+
if (typeof BroadcastChannel === "undefined") return () => {
|
|
249
|
+
};
|
|
250
|
+
let channel;
|
|
251
|
+
try {
|
|
252
|
+
channel = new BroadcastChannel(BC_CHANNEL);
|
|
253
|
+
} catch {
|
|
254
|
+
return () => {
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
channel.onmessage = (e) => {
|
|
258
|
+
if (e.data?.type === "session_claim") {
|
|
259
|
+
channel.postMessage({ type: "session_adopt", session_id: sessionId });
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
return () => channel.close();
|
|
263
|
+
}
|
|
149
264
|
async function getOrCreateSession(apiUrl, apiKey, visitorId) {
|
|
150
265
|
const storedSessionId = localStorage.getItem("af_session_id");
|
|
151
266
|
const lastActivity = localStorage.getItem("af_session_last_activity");
|
|
@@ -160,11 +275,20 @@ async function getOrCreateSession(apiUrl, apiKey, visitorId) {
|
|
|
160
275
|
localStorage.removeItem("af_session_last_activity");
|
|
161
276
|
}
|
|
162
277
|
if (_sessionFlight) return _sessionFlight;
|
|
163
|
-
_sessionFlight =
|
|
278
|
+
_sessionFlight = (async () => {
|
|
279
|
+
const adopted = await requestSessionFromPeer();
|
|
280
|
+
if (adopted) {
|
|
281
|
+
localStorage.setItem("af_session_id", adopted);
|
|
282
|
+
localStorage.setItem("af_session_last_activity", String(Date.now()));
|
|
283
|
+
return adopted;
|
|
284
|
+
}
|
|
285
|
+
const id = await createSession(apiUrl, apiKey, visitorId);
|
|
164
286
|
if (id) {
|
|
165
287
|
localStorage.setItem("af_session_id", id);
|
|
166
288
|
localStorage.setItem("af_session_last_activity", String(Date.now()));
|
|
167
289
|
}
|
|
290
|
+
return id;
|
|
291
|
+
})().then((id) => {
|
|
168
292
|
_sessionFlight = null;
|
|
169
293
|
return id;
|
|
170
294
|
}).catch((err) => {
|
|
@@ -173,11 +297,18 @@ async function getOrCreateSession(apiUrl, apiKey, visitorId) {
|
|
|
173
297
|
});
|
|
174
298
|
return _sessionFlight;
|
|
175
299
|
}
|
|
176
|
-
function useAnalytics(apiUrl, apiKey) {
|
|
300
|
+
function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
177
301
|
const sessionId = useRef(null);
|
|
178
302
|
const pendingEvents = useRef([]);
|
|
179
|
-
const pathname = useRef(typeof window !== "undefined" ? window?.location?.pathname ?? "" : "");
|
|
180
303
|
const hasSentEnd = useRef(false);
|
|
304
|
+
const presenceRef = useRef(null);
|
|
305
|
+
const closeResponderRef = useRef(() => {
|
|
306
|
+
});
|
|
307
|
+
const idleTimerRef = useRef(null);
|
|
308
|
+
const currentPathRef = useRef(currentPath);
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
currentPathRef.current = currentPath;
|
|
311
|
+
}, [currentPath]);
|
|
181
312
|
useEffect(() => {
|
|
182
313
|
if (typeof window === "undefined") return;
|
|
183
314
|
const visitor_id = localStorage.getItem("af_analytics_visitor_id") || crypto.randomUUID();
|
|
@@ -186,6 +317,15 @@ function useAnalytics(apiUrl, apiKey) {
|
|
|
186
317
|
getOrCreateSession(apiUrl, apiKey, visitor_id).then((id) => {
|
|
187
318
|
if (cancelled || !id) return;
|
|
188
319
|
sessionId.current = id;
|
|
320
|
+
closeResponderRef.current = openSessionResponder(id);
|
|
321
|
+
idleTimerRef.current = setInterval(() => {
|
|
322
|
+
if (!hasSentEnd.current) {
|
|
323
|
+
localStorage.setItem("af_session_last_activity", String(Date.now()));
|
|
324
|
+
}
|
|
325
|
+
}, IDLE_TOUCH_INTERVAL_MS);
|
|
326
|
+
if (wsUrl) {
|
|
327
|
+
presenceRef.current = connectPresence(apiKey, id, wsUrl);
|
|
328
|
+
}
|
|
189
329
|
pendingEvents.current.forEach((fn) => fn());
|
|
190
330
|
pendingEvents.current = [];
|
|
191
331
|
});
|
|
@@ -198,19 +338,84 @@ function useAnalytics(apiUrl, apiKey) {
|
|
|
198
338
|
const sendEnd = () => {
|
|
199
339
|
if (!sessionId.current || hasSentEnd.current) return;
|
|
200
340
|
hasSentEnd.current = true;
|
|
341
|
+
if (idleTimerRef.current !== null) {
|
|
342
|
+
clearInterval(idleTimerRef.current);
|
|
343
|
+
idleTimerRef.current = null;
|
|
344
|
+
}
|
|
345
|
+
closeResponderRef.current();
|
|
346
|
+
presenceRef.current?.disconnect();
|
|
347
|
+
presenceRef.current = null;
|
|
201
348
|
navigator.sendBeacon(`${apiUrl}/sessions/${sessionId.current}/end`);
|
|
202
349
|
localStorage.removeItem("af_session_id");
|
|
203
350
|
localStorage.removeItem("af_session_last_activity");
|
|
204
351
|
};
|
|
352
|
+
const restartSession = () => {
|
|
353
|
+
sessionId.current = null;
|
|
354
|
+
hasSentEnd.current = false;
|
|
355
|
+
if (idleTimerRef.current !== null) {
|
|
356
|
+
clearInterval(idleTimerRef.current);
|
|
357
|
+
idleTimerRef.current = null;
|
|
358
|
+
}
|
|
359
|
+
closeResponderRef.current();
|
|
360
|
+
closeResponderRef.current = () => {
|
|
361
|
+
};
|
|
362
|
+
presenceRef.current?.disconnect();
|
|
363
|
+
presenceRef.current = null;
|
|
364
|
+
const visitor_id = localStorage.getItem("af_analytics_visitor_id") || crypto.randomUUID();
|
|
365
|
+
getOrCreateSession(apiUrl, apiKey, visitor_id).then((id) => {
|
|
366
|
+
if (!id) return;
|
|
367
|
+
sessionId.current = id;
|
|
368
|
+
closeResponderRef.current = openSessionResponder(id);
|
|
369
|
+
idleTimerRef.current = setInterval(() => {
|
|
370
|
+
if (!hasSentEnd.current) {
|
|
371
|
+
localStorage.setItem("af_session_last_activity", String(Date.now()));
|
|
372
|
+
}
|
|
373
|
+
}, IDLE_TOUCH_INTERVAL_MS);
|
|
374
|
+
if (wsUrl) {
|
|
375
|
+
presenceRef.current = connectPresence(apiKey, id, wsUrl);
|
|
376
|
+
}
|
|
377
|
+
trackPageview(apiUrl, apiKey, id, currentPathRef.current);
|
|
378
|
+
});
|
|
379
|
+
};
|
|
380
|
+
const handleStorage = (e) => {
|
|
381
|
+
if (e.key === "af_session_id" && e.newValue === null && e.oldValue !== null) {
|
|
382
|
+
restartSession();
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
const handlePageHide = (e) => {
|
|
386
|
+
if (!e.persisted) sendEnd();
|
|
387
|
+
};
|
|
388
|
+
const handlePageShow = (e) => {
|
|
389
|
+
if (!e.persisted) return;
|
|
390
|
+
if (hasSentEnd.current || !sessionId.current) {
|
|
391
|
+
restartSession();
|
|
392
|
+
} else {
|
|
393
|
+
trackPageview(apiUrl, apiKey, sessionId.current, currentPathRef.current);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
let visHideTimer = null;
|
|
205
397
|
const handleVisibility = () => {
|
|
206
|
-
if (document.visibilityState === "hidden")
|
|
398
|
+
if (document.visibilityState === "hidden") {
|
|
399
|
+
visHideTimer = setTimeout(sendEnd, 5e3);
|
|
400
|
+
} else {
|
|
401
|
+
if (visHideTimer !== null) {
|
|
402
|
+
clearTimeout(visHideTimer);
|
|
403
|
+
visHideTimer = null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
207
406
|
};
|
|
208
|
-
|
|
407
|
+
window.addEventListener("storage", handleStorage);
|
|
209
408
|
document.addEventListener("visibilitychange", handleVisibility);
|
|
210
|
-
window.addEventListener("
|
|
409
|
+
window.addEventListener("pagehide", handlePageHide);
|
|
410
|
+
window.addEventListener("pageshow", handlePageShow);
|
|
411
|
+
window.addEventListener("beforeunload", sendEnd);
|
|
211
412
|
return () => {
|
|
413
|
+
if (visHideTimer !== null) clearTimeout(visHideTimer);
|
|
414
|
+
window.removeEventListener("storage", handleStorage);
|
|
212
415
|
document.removeEventListener("visibilitychange", handleVisibility);
|
|
213
|
-
window.removeEventListener("
|
|
416
|
+
window.removeEventListener("pagehide", handlePageHide);
|
|
417
|
+
window.removeEventListener("pageshow", handlePageShow);
|
|
418
|
+
window.removeEventListener("beforeunload", sendEnd);
|
|
214
419
|
};
|
|
215
420
|
}, [apiUrl]);
|
|
216
421
|
function enqueueOrRun(fn) {
|
|
@@ -226,38 +431,39 @@ function useAnalytics(apiUrl, apiKey) {
|
|
|
226
431
|
enqueueOrRun(() => trackPageview(apiUrl, apiKey, sessionId.current, path));
|
|
227
432
|
},
|
|
228
433
|
trackClick: (e, element) => {
|
|
229
|
-
enqueueOrRun(() => trackClick(apiUrl, apiKey, sessionId.current,
|
|
434
|
+
enqueueOrRun(() => trackClick(apiUrl, apiKey, sessionId.current, currentPath, e, element));
|
|
230
435
|
},
|
|
231
436
|
trackScroll: (depth) => {
|
|
232
|
-
enqueueOrRun(() => trackScroll(apiUrl, apiKey, sessionId.current,
|
|
437
|
+
enqueueOrRun(() => trackScroll(apiUrl, apiKey, sessionId.current, currentPath, depth));
|
|
233
438
|
},
|
|
234
439
|
trackCopy: () => {
|
|
235
|
-
enqueueOrRun(() => trackCopy(apiUrl, apiKey, sessionId.current,
|
|
440
|
+
enqueueOrRun(() => trackCopy(apiUrl, apiKey, sessionId.current, currentPath));
|
|
236
441
|
},
|
|
237
442
|
trackException: (fields) => {
|
|
238
|
-
enqueueOrRun(() => trackException(apiUrl, apiKey, sessionId.current,
|
|
443
|
+
enqueueOrRun(() => trackException(apiUrl, apiKey, sessionId.current, currentPath, fields));
|
|
239
444
|
},
|
|
240
445
|
trackError: (error) => {
|
|
241
|
-
enqueueOrRun(() => trackError(apiUrl, apiKey, sessionId.current,
|
|
446
|
+
enqueueOrRun(() => trackError(apiUrl, apiKey, sessionId.current, currentPath, error));
|
|
242
447
|
},
|
|
243
448
|
trackCTA: (ctaId, ctaVariant) => {
|
|
244
|
-
enqueueOrRun(() => trackCTA(apiUrl, apiKey, sessionId.current,
|
|
449
|
+
enqueueOrRun(() => trackCTA(apiUrl, apiKey, sessionId.current, currentPath, ctaId, ctaVariant));
|
|
245
450
|
},
|
|
246
451
|
trackSearch: (query) => {
|
|
247
|
-
enqueueOrRun(() => trackSearch(apiUrl, apiKey, sessionId.current,
|
|
452
|
+
enqueueOrRun(() => trackSearch(apiUrl, apiKey, sessionId.current, currentPath, query));
|
|
248
453
|
}
|
|
249
454
|
};
|
|
250
455
|
}
|
|
251
456
|
|
|
252
457
|
// src/components/Analytics.tsx
|
|
253
|
-
function Analytics({ apiKey, apiUrl }) {
|
|
254
|
-
const
|
|
458
|
+
function Analytics({ apiKey, apiUrl, wsUrl }) {
|
|
459
|
+
const pathname = usePathname();
|
|
460
|
+
const { trackPageview: trackPageview2, trackClick: trackClick2, trackScroll: trackScroll2, trackCopy: trackCopy2, trackException: trackException2 } = useAnalytics(apiUrl, apiKey, pathname, wsUrl);
|
|
255
461
|
const lastTracked = useRef2(null);
|
|
256
462
|
const lastScrollDepth = useRef2(0);
|
|
257
|
-
const pathname = typeof window !== "undefined" ? window.location.pathname : "";
|
|
258
463
|
useEffect2(() => {
|
|
259
464
|
if (lastTracked.current === pathname) return;
|
|
260
465
|
lastTracked.current = pathname;
|
|
466
|
+
lastScrollDepth.current = 0;
|
|
261
467
|
trackPageview2(pathname);
|
|
262
468
|
}, [pathname, trackPageview2]);
|
|
263
469
|
useEffect2(() => {
|
|
@@ -269,12 +475,10 @@ function Analytics({ apiKey, apiUrl }) {
|
|
|
269
475
|
useEffect2(() => {
|
|
270
476
|
if (typeof window === "undefined") return;
|
|
271
477
|
const handler = () => {
|
|
272
|
-
const scrolled = window.scrollY;
|
|
273
478
|
const total = document.documentElement.scrollHeight - window.innerHeight;
|
|
274
479
|
if (total <= 0) return;
|
|
275
|
-
const depth = Math.round(
|
|
276
|
-
const
|
|
277
|
-
for (const m of milestones) {
|
|
480
|
+
const depth = Math.round(window.scrollY / total * 100);
|
|
481
|
+
for (const m of [25, 50, 75, 100]) {
|
|
278
482
|
if (depth >= m && lastScrollDepth.current < m) {
|
|
279
483
|
lastScrollDepth.current = m;
|
|
280
484
|
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
|
}
|