@arthurreira/analytics 0.16.0 → 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 -2
- package/dist/client.d.ts +3 -2
- package/dist/client.js +136 -35
- package/dist/index.d.ts +1 -1
- package/dist/index.js +41 -12
- 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) {
|
|
@@ -141,6 +141,7 @@ var AfAnalytics = (() => {
|
|
|
141
141
|
headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
|
|
142
142
|
body: JSON.stringify(body)
|
|
143
143
|
});
|
|
144
|
+
if (!res.ok) return "";
|
|
144
145
|
const data = await res.json();
|
|
145
146
|
return data.id;
|
|
146
147
|
}
|
|
@@ -201,6 +202,28 @@ var AfAnalytics = (() => {
|
|
|
201
202
|
}).catch(() => {
|
|
202
203
|
});
|
|
203
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
|
+
});
|
|
204
227
|
window.addEventListener("click", (e) => {
|
|
205
228
|
enqueue((id) => trackClick(apiUrl, apiKey, id, e));
|
|
206
229
|
});
|
|
@@ -246,9 +269,18 @@ var AfAnalytics = (() => {
|
|
|
246
269
|
navigator.sendBeacon(`${apiUrl}/sessions/${sessionId}/end`);
|
|
247
270
|
clearSession();
|
|
248
271
|
};
|
|
272
|
+
let visHideTimer = null;
|
|
249
273
|
document.addEventListener("visibilitychange", () => {
|
|
250
|
-
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
|
+
}
|
|
251
282
|
});
|
|
283
|
+
window.addEventListener("pagehide", endSession);
|
|
252
284
|
window.addEventListener("beforeunload", endSession);
|
|
253
285
|
}
|
|
254
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 {
|
|
@@ -51,16 +52,29 @@ async function createSession(apiUrl, apiKey, visitorId) {
|
|
|
51
52
|
sessionData.utm_term = params.get("utm_term");
|
|
52
53
|
sessionData.utm_content = params.get("utm_content");
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
console.log(`[AF Analytics SDK] Creating session at ${apiUrl}/sessions with API key: ${apiKey.slice(0, 10)}...`);
|
|
56
|
+
try {
|
|
57
|
+
const response = await fetch(`${apiUrl}/sessions`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
"X-API-Key": apiKey,
|
|
61
|
+
"Content-Type": "application/json"
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify(sessionData)
|
|
64
|
+
});
|
|
65
|
+
console.log(`[AF Analytics SDK] Session creation response: ${response.status} ${response.statusText}`);
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const errText = await response.text();
|
|
68
|
+
console.error(`[AF Analytics SDK] Session creation failed: ${errText}`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const data = await response.json();
|
|
72
|
+
console.log(`[AF Analytics SDK] Session created: ${data.id}`);
|
|
73
|
+
return data.id ?? null;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error(`[AF Analytics SDK] Session creation error: ${err}`);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
64
78
|
}
|
|
65
79
|
async function sendEvent(apiUrl, apiKey, payload) {
|
|
66
80
|
await fetch(`${apiUrl}/events`, {
|
|
@@ -75,7 +89,6 @@ async function sendEvent(apiUrl, apiKey, payload) {
|
|
|
75
89
|
async function trackPageview(apiUrl, apiKey, sessionId, path) {
|
|
76
90
|
await sendEvent(apiUrl, apiKey, {
|
|
77
91
|
...BASE_FIELDS(sessionId, "pageview", path),
|
|
78
|
-
page_url: `${typeof window !== "undefined" ? window.location.origin : ""}${path}`,
|
|
79
92
|
page_title: typeof document !== "undefined" ? document.title || null : null,
|
|
80
93
|
referrer: typeof document !== "undefined" ? document.referrer || null : null,
|
|
81
94
|
scroll_depth: 0
|
|
@@ -130,6 +143,77 @@ async function trackCTA(apiUrl, apiKey, sessionId, path, ctaId, ctaVariant) {
|
|
|
130
143
|
});
|
|
131
144
|
}
|
|
132
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
|
+
|
|
133
217
|
// src/hooks/useAnalytics.ts
|
|
134
218
|
var SESSION_EXPIRY_MINUTES = 30;
|
|
135
219
|
var _sessionFlight = null;
|
|
@@ -143,11 +227,15 @@ async function getOrCreateSession(apiUrl, apiKey, visitorId) {
|
|
|
143
227
|
localStorage.setItem("af_session_last_activity", String(now));
|
|
144
228
|
return storedSessionId;
|
|
145
229
|
}
|
|
230
|
+
localStorage.removeItem("af_session_id");
|
|
231
|
+
localStorage.removeItem("af_session_last_activity");
|
|
146
232
|
}
|
|
147
233
|
if (_sessionFlight) return _sessionFlight;
|
|
148
234
|
_sessionFlight = createSession(apiUrl, apiKey, visitorId).then((id) => {
|
|
149
|
-
|
|
150
|
-
|
|
235
|
+
if (id) {
|
|
236
|
+
localStorage.setItem("af_session_id", id);
|
|
237
|
+
localStorage.setItem("af_session_last_activity", String(Date.now()));
|
|
238
|
+
}
|
|
151
239
|
_sessionFlight = null;
|
|
152
240
|
return id;
|
|
153
241
|
}).catch((err) => {
|
|
@@ -156,19 +244,22 @@ async function getOrCreateSession(apiUrl, apiKey, visitorId) {
|
|
|
156
244
|
});
|
|
157
245
|
return _sessionFlight;
|
|
158
246
|
}
|
|
159
|
-
function useAnalytics(apiUrl, apiKey) {
|
|
247
|
+
function useAnalytics(apiUrl, apiKey, currentPath, wsUrl) {
|
|
160
248
|
const sessionId = useRef(null);
|
|
161
249
|
const pendingEvents = useRef([]);
|
|
162
|
-
const pathname = useRef(typeof window !== "undefined" ? window?.location?.pathname ?? "" : "");
|
|
163
250
|
const hasSentEnd = useRef(false);
|
|
251
|
+
const presenceRef = useRef(null);
|
|
164
252
|
useEffect(() => {
|
|
165
253
|
if (typeof window === "undefined") return;
|
|
166
254
|
const visitor_id = localStorage.getItem("af_analytics_visitor_id") || crypto.randomUUID();
|
|
167
255
|
localStorage.setItem("af_analytics_visitor_id", visitor_id);
|
|
168
256
|
let cancelled = false;
|
|
169
257
|
getOrCreateSession(apiUrl, apiKey, visitor_id).then((id) => {
|
|
170
|
-
if (cancelled) return;
|
|
258
|
+
if (cancelled || !id) return;
|
|
171
259
|
sessionId.current = id;
|
|
260
|
+
if (wsUrl) {
|
|
261
|
+
presenceRef.current = connectPresence(apiKey, id, wsUrl);
|
|
262
|
+
}
|
|
172
263
|
pendingEvents.current.forEach((fn) => fn());
|
|
173
264
|
pendingEvents.current = [];
|
|
174
265
|
});
|
|
@@ -181,19 +272,30 @@ function useAnalytics(apiUrl, apiKey) {
|
|
|
181
272
|
const sendEnd = () => {
|
|
182
273
|
if (!sessionId.current || hasSentEnd.current) return;
|
|
183
274
|
hasSentEnd.current = true;
|
|
275
|
+
presenceRef.current?.disconnect();
|
|
184
276
|
navigator.sendBeacon(`${apiUrl}/sessions/${sessionId.current}/end`);
|
|
185
277
|
localStorage.removeItem("af_session_id");
|
|
186
278
|
localStorage.removeItem("af_session_last_activity");
|
|
187
279
|
};
|
|
280
|
+
let visHideTimer = null;
|
|
188
281
|
const handleVisibility = () => {
|
|
189
|
-
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
|
+
}
|
|
190
290
|
};
|
|
191
|
-
const handleUnload = () => sendEnd();
|
|
192
291
|
document.addEventListener("visibilitychange", handleVisibility);
|
|
193
|
-
window.addEventListener("
|
|
292
|
+
window.addEventListener("pagehide", sendEnd);
|
|
293
|
+
window.addEventListener("beforeunload", sendEnd);
|
|
194
294
|
return () => {
|
|
295
|
+
if (visHideTimer !== null) clearTimeout(visHideTimer);
|
|
195
296
|
document.removeEventListener("visibilitychange", handleVisibility);
|
|
196
|
-
window.removeEventListener("
|
|
297
|
+
window.removeEventListener("pagehide", sendEnd);
|
|
298
|
+
window.removeEventListener("beforeunload", sendEnd);
|
|
197
299
|
};
|
|
198
300
|
}, [apiUrl]);
|
|
199
301
|
function enqueueOrRun(fn) {
|
|
@@ -209,38 +311,39 @@ function useAnalytics(apiUrl, apiKey) {
|
|
|
209
311
|
enqueueOrRun(() => trackPageview(apiUrl, apiKey, sessionId.current, path));
|
|
210
312
|
},
|
|
211
313
|
trackClick: (e, element) => {
|
|
212
|
-
enqueueOrRun(() => trackClick(apiUrl, apiKey, sessionId.current,
|
|
314
|
+
enqueueOrRun(() => trackClick(apiUrl, apiKey, sessionId.current, currentPath, e, element));
|
|
213
315
|
},
|
|
214
316
|
trackScroll: (depth) => {
|
|
215
|
-
enqueueOrRun(() => trackScroll(apiUrl, apiKey, sessionId.current,
|
|
317
|
+
enqueueOrRun(() => trackScroll(apiUrl, apiKey, sessionId.current, currentPath, depth));
|
|
216
318
|
},
|
|
217
319
|
trackCopy: () => {
|
|
218
|
-
enqueueOrRun(() => trackCopy(apiUrl, apiKey, sessionId.current,
|
|
320
|
+
enqueueOrRun(() => trackCopy(apiUrl, apiKey, sessionId.current, currentPath));
|
|
219
321
|
},
|
|
220
322
|
trackException: (fields) => {
|
|
221
|
-
enqueueOrRun(() => trackException(apiUrl, apiKey, sessionId.current,
|
|
323
|
+
enqueueOrRun(() => trackException(apiUrl, apiKey, sessionId.current, currentPath, fields));
|
|
222
324
|
},
|
|
223
325
|
trackError: (error) => {
|
|
224
|
-
enqueueOrRun(() => trackError(apiUrl, apiKey, sessionId.current,
|
|
326
|
+
enqueueOrRun(() => trackError(apiUrl, apiKey, sessionId.current, currentPath, error));
|
|
225
327
|
},
|
|
226
328
|
trackCTA: (ctaId, ctaVariant) => {
|
|
227
|
-
enqueueOrRun(() => trackCTA(apiUrl, apiKey, sessionId.current,
|
|
329
|
+
enqueueOrRun(() => trackCTA(apiUrl, apiKey, sessionId.current, currentPath, ctaId, ctaVariant));
|
|
228
330
|
},
|
|
229
331
|
trackSearch: (query) => {
|
|
230
|
-
enqueueOrRun(() => trackSearch(apiUrl, apiKey, sessionId.current,
|
|
332
|
+
enqueueOrRun(() => trackSearch(apiUrl, apiKey, sessionId.current, currentPath, query));
|
|
231
333
|
}
|
|
232
334
|
};
|
|
233
335
|
}
|
|
234
336
|
|
|
235
337
|
// src/components/Analytics.tsx
|
|
236
|
-
function Analytics({ apiKey, apiUrl }) {
|
|
237
|
-
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);
|
|
238
341
|
const lastTracked = useRef2(null);
|
|
239
342
|
const lastScrollDepth = useRef2(0);
|
|
240
|
-
const pathname = typeof window !== "undefined" ? window.location.pathname : "";
|
|
241
343
|
useEffect2(() => {
|
|
242
344
|
if (lastTracked.current === pathname) return;
|
|
243
345
|
lastTracked.current = pathname;
|
|
346
|
+
lastScrollDepth.current = 0;
|
|
244
347
|
trackPageview2(pathname);
|
|
245
348
|
}, [pathname, trackPageview2]);
|
|
246
349
|
useEffect2(() => {
|
|
@@ -252,12 +355,10 @@ function Analytics({ apiKey, apiUrl }) {
|
|
|
252
355
|
useEffect2(() => {
|
|
253
356
|
if (typeof window === "undefined") return;
|
|
254
357
|
const handler = () => {
|
|
255
|
-
const scrolled = window.scrollY;
|
|
256
358
|
const total = document.documentElement.scrollHeight - window.innerHeight;
|
|
257
359
|
if (total <= 0) return;
|
|
258
|
-
const depth = Math.round(
|
|
259
|
-
const
|
|
260
|
-
for (const m of milestones) {
|
|
360
|
+
const depth = Math.round(window.scrollY / total * 100);
|
|
361
|
+
for (const m of [25, 50, 75, 100]) {
|
|
261
362
|
if (depth >= m && lastScrollDepth.current < m) {
|
|
262
363
|
lastScrollDepth.current = m;
|
|
263
364
|
trackScroll2(m);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
declare function createSession(apiUrl: string, apiKey: string, visitorId?: string): Promise<
|
|
1
|
+
declare function createSession(apiUrl: string, apiKey: string, visitorId?: string): Promise<string | null>;
|
|
2
2
|
declare function trackPageview(apiUrl: string, apiKey: string, sessionId: string, path: string): Promise<void>;
|
|
3
3
|
declare function trackClick(apiUrl: string, apiKey: string, sessionId: string, path: string, e: MouseEvent, element: HTMLElement): Promise<void>;
|
|
4
4
|
declare function trackScroll(apiUrl: string, apiKey: string, sessionId: string, path: string, depth: number): Promise<void>;
|
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 {
|
|
@@ -42,16 +42,29 @@ async function createSession(apiUrl, apiKey, visitorId) {
|
|
|
42
42
|
sessionData.utm_term = params.get("utm_term");
|
|
43
43
|
sessionData.utm_content = params.get("utm_content");
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
console.log(`[AF Analytics SDK] Creating session at ${apiUrl}/sessions with API key: ${apiKey.slice(0, 10)}...`);
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch(`${apiUrl}/sessions`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: {
|
|
50
|
+
"X-API-Key": apiKey,
|
|
51
|
+
"Content-Type": "application/json"
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify(sessionData)
|
|
54
|
+
});
|
|
55
|
+
console.log(`[AF Analytics SDK] Session creation response: ${response.status} ${response.statusText}`);
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
const errText = await response.text();
|
|
58
|
+
console.error(`[AF Analytics SDK] Session creation failed: ${errText}`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const data = await response.json();
|
|
62
|
+
console.log(`[AF Analytics SDK] Session created: ${data.id}`);
|
|
63
|
+
return data.id ?? null;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(`[AF Analytics SDK] Session creation error: ${err}`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
55
68
|
}
|
|
56
69
|
async function sendEvent(apiUrl, apiKey, payload) {
|
|
57
70
|
await fetch(`${apiUrl}/events`, {
|
|
@@ -66,7 +79,6 @@ async function sendEvent(apiUrl, apiKey, payload) {
|
|
|
66
79
|
async function trackPageview(apiUrl, apiKey, sessionId, path) {
|
|
67
80
|
await sendEvent(apiUrl, apiKey, {
|
|
68
81
|
...BASE_FIELDS(sessionId, "pageview", path),
|
|
69
|
-
page_url: `${typeof window !== "undefined" ? window.location.origin : ""}${path}`,
|
|
70
82
|
page_title: typeof document !== "undefined" ? document.title || null : null,
|
|
71
83
|
referrer: typeof document !== "undefined" ? document.referrer || null : null,
|
|
72
84
|
scroll_depth: 0
|
|
@@ -132,6 +144,7 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
|
|
|
132
144
|
let attempts = 0;
|
|
133
145
|
let intentionalClose = false;
|
|
134
146
|
let retryTimer = null;
|
|
147
|
+
let pingInterval = null;
|
|
135
148
|
let targetUrl;
|
|
136
149
|
try {
|
|
137
150
|
const u = new URL(wsUrl);
|
|
@@ -142,6 +155,12 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
|
|
|
142
155
|
return { disconnect: () => {
|
|
143
156
|
} };
|
|
144
157
|
}
|
|
158
|
+
function clearPing() {
|
|
159
|
+
if (pingInterval !== null) {
|
|
160
|
+
clearInterval(pingInterval);
|
|
161
|
+
pingInterval = null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
145
164
|
function connect() {
|
|
146
165
|
try {
|
|
147
166
|
ws = new WebSocket(targetUrl);
|
|
@@ -151,8 +170,14 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
|
|
|
151
170
|
ws.onopen = () => {
|
|
152
171
|
attempts = 0;
|
|
153
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);
|
|
154
178
|
};
|
|
155
179
|
ws.onclose = () => {
|
|
180
|
+
clearPing();
|
|
156
181
|
if (intentionalClose || attempts >= 3) return;
|
|
157
182
|
const delay = Math.pow(2, attempts) * 1e3;
|
|
158
183
|
attempts++;
|
|
@@ -163,11 +188,15 @@ function connectPresence(apiKey, sessionId, wsUrl = DEFAULT_WS_URL) {
|
|
|
163
188
|
return {
|
|
164
189
|
disconnect: () => {
|
|
165
190
|
intentionalClose = true;
|
|
191
|
+
clearPing();
|
|
166
192
|
if (retryTimer !== null) {
|
|
167
193
|
clearTimeout(retryTimer);
|
|
168
194
|
retryTimer = null;
|
|
169
195
|
}
|
|
170
196
|
if (ws) {
|
|
197
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
198
|
+
ws.send(JSON.stringify({ type: "leave", session_id: sessionId }));
|
|
199
|
+
}
|
|
171
200
|
ws.close();
|
|
172
201
|
ws = null;
|
|
173
202
|
}
|