@outlit/browser 0.2.0 → 0.2.2
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/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +498 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +492 -12
- package/dist/index.mjs.map +1 -1
- package/dist/outlit.global.js +1 -1
- package/dist/outlit.global.js.map +1 -1
- package/dist/react/index.d.mts +12 -12
- package/dist/react/index.d.ts +12 -12
- package/dist/react/index.js +512 -33
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +507 -27
- package/dist/react/index.mjs.map +1 -1
- package/dist/{tracker-CocH64L9.d.mts → tracker-DFcTv3EM.d.mts} +39 -9
- package/dist/{tracker-CocH64L9.d.ts → tracker-DFcTv3EM.d.ts} +39 -9
- package/package.json +3 -3
package/dist/react/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { createContext, useCallback, useEffect, useRef, useState } from "react";
|
|
|
4
4
|
// src/tracker.ts
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_API_HOST,
|
|
7
|
+
buildCalendarEvent,
|
|
7
8
|
buildCustomEvent,
|
|
8
9
|
buildFormEvent,
|
|
9
10
|
buildIdentifyEvent,
|
|
@@ -29,19 +30,22 @@ function capturePageview() {
|
|
|
29
30
|
lastUrl = url;
|
|
30
31
|
pageviewCallback(url, referrer, title);
|
|
31
32
|
}
|
|
33
|
+
function capturePageviewDelayed() {
|
|
34
|
+
setTimeout(capturePageview, 10);
|
|
35
|
+
}
|
|
32
36
|
function setupSpaListeners() {
|
|
33
37
|
window.addEventListener("popstate", () => {
|
|
34
|
-
|
|
38
|
+
capturePageviewDelayed();
|
|
35
39
|
});
|
|
36
40
|
const originalPushState = history.pushState;
|
|
37
41
|
const originalReplaceState = history.replaceState;
|
|
38
42
|
history.pushState = function(...args) {
|
|
39
43
|
originalPushState.apply(this, args);
|
|
40
|
-
|
|
44
|
+
capturePageviewDelayed();
|
|
41
45
|
};
|
|
42
46
|
history.replaceState = function(...args) {
|
|
43
47
|
originalReplaceState.apply(this, args);
|
|
44
|
-
|
|
48
|
+
capturePageviewDelayed();
|
|
45
49
|
};
|
|
46
50
|
}
|
|
47
51
|
var formCallback = null;
|
|
@@ -92,6 +96,431 @@ function stopAutocapture() {
|
|
|
92
96
|
document.removeEventListener("submit", handleFormSubmit, true);
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
// src/embed-integrations.ts
|
|
100
|
+
var callbacks = null;
|
|
101
|
+
var isListening = false;
|
|
102
|
+
var calSetupAttempts = 0;
|
|
103
|
+
var calCallbackRegistered = false;
|
|
104
|
+
var lastBookingUid = null;
|
|
105
|
+
var CAL_MAX_RETRY_ATTEMPTS = 10;
|
|
106
|
+
var CAL_INITIAL_DELAY_MS = 200;
|
|
107
|
+
var CAL_MAX_DELAY_MS = 2e3;
|
|
108
|
+
function parseCalComBooking(data) {
|
|
109
|
+
const event = {
|
|
110
|
+
provider: "cal.com"
|
|
111
|
+
};
|
|
112
|
+
if (data.title) {
|
|
113
|
+
event.eventType = data.title;
|
|
114
|
+
const nameMatch = data.title.match(/between .+ and (.+)$/i);
|
|
115
|
+
if (nameMatch?.[1]) {
|
|
116
|
+
event.inviteeName = nameMatch[1].trim();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (data.startTime) event.startTime = data.startTime;
|
|
120
|
+
if (data.endTime) event.endTime = data.endTime;
|
|
121
|
+
if (data.startTime && data.endTime) {
|
|
122
|
+
const start = new Date(data.startTime);
|
|
123
|
+
const end = new Date(data.endTime);
|
|
124
|
+
event.duration = Math.round((end.getTime() - start.getTime()) / 6e4);
|
|
125
|
+
}
|
|
126
|
+
if (data.isRecurring !== void 0) {
|
|
127
|
+
event.isRecurring = data.isRecurring;
|
|
128
|
+
}
|
|
129
|
+
return event;
|
|
130
|
+
}
|
|
131
|
+
function setupCalComListener() {
|
|
132
|
+
if (typeof window === "undefined") return;
|
|
133
|
+
if (calCallbackRegistered) return;
|
|
134
|
+
calSetupAttempts++;
|
|
135
|
+
if ("Cal" in window) {
|
|
136
|
+
const Cal = window.Cal;
|
|
137
|
+
if (typeof Cal === "function") {
|
|
138
|
+
try {
|
|
139
|
+
Cal("on", {
|
|
140
|
+
action: "bookingSuccessfulV2",
|
|
141
|
+
callback: handleCalComBooking
|
|
142
|
+
});
|
|
143
|
+
calCallbackRegistered = true;
|
|
144
|
+
return;
|
|
145
|
+
} catch (_e) {
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (calSetupAttempts < CAL_MAX_RETRY_ATTEMPTS) {
|
|
150
|
+
const delay = Math.min(CAL_INITIAL_DELAY_MS * calSetupAttempts, CAL_MAX_DELAY_MS);
|
|
151
|
+
setTimeout(setupCalComListener, delay);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function handleCalComBooking(e) {
|
|
155
|
+
if (!callbacks) return;
|
|
156
|
+
const data = e.detail?.data;
|
|
157
|
+
if (!data) return;
|
|
158
|
+
if (data.uid && data.uid === lastBookingUid) return;
|
|
159
|
+
lastBookingUid = data.uid || null;
|
|
160
|
+
const bookingEvent = parseCalComBooking(data);
|
|
161
|
+
callbacks.onCalendarBooked(bookingEvent);
|
|
162
|
+
}
|
|
163
|
+
function handlePostMessage(event) {
|
|
164
|
+
if (!callbacks) return;
|
|
165
|
+
if (isCalendlyEvent(event)) {
|
|
166
|
+
if (event.data.event === "calendly.event_scheduled") {
|
|
167
|
+
const bookingEvent = parseCalendlyBooking(event.data.payload);
|
|
168
|
+
callbacks.onCalendarBooked(bookingEvent);
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (isCalComRawMessage(event)) {
|
|
173
|
+
const bookingData = extractCalComBookingFromMessage(event.data);
|
|
174
|
+
if (bookingData) {
|
|
175
|
+
if (bookingData.uid && bookingData.uid === lastBookingUid) return;
|
|
176
|
+
lastBookingUid = bookingData.uid || null;
|
|
177
|
+
const bookingEvent = parseCalComBooking(bookingData);
|
|
178
|
+
callbacks.onCalendarBooked(bookingEvent);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function isCalComRawMessage(event) {
|
|
183
|
+
if (!event.origin.includes("cal.com")) return false;
|
|
184
|
+
const data = event.data;
|
|
185
|
+
if (!data || typeof data !== "object") return false;
|
|
186
|
+
const messageType = data.type || data.action;
|
|
187
|
+
return messageType === "bookingSuccessfulV2" || messageType === "bookingSuccessful" || messageType === "booking_successful";
|
|
188
|
+
}
|
|
189
|
+
function extractCalComBookingFromMessage(data) {
|
|
190
|
+
if (!data || typeof data !== "object") return null;
|
|
191
|
+
const messageData = data;
|
|
192
|
+
if (messageData.data && typeof messageData.data === "object") {
|
|
193
|
+
return messageData.data;
|
|
194
|
+
}
|
|
195
|
+
if (messageData.booking && typeof messageData.booking === "object") {
|
|
196
|
+
return messageData.booking;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
function isCalendlyEvent(e) {
|
|
201
|
+
return e.origin === "https://calendly.com" && e.data && typeof e.data.event === "string" && e.data.event.startsWith("calendly.");
|
|
202
|
+
}
|
|
203
|
+
function parseCalendlyBooking(_payload) {
|
|
204
|
+
return {
|
|
205
|
+
provider: "calendly"
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function initCalendarTracking(cbs) {
|
|
209
|
+
if (isListening) return;
|
|
210
|
+
callbacks = cbs;
|
|
211
|
+
isListening = true;
|
|
212
|
+
calSetupAttempts = 0;
|
|
213
|
+
window.addEventListener("message", handlePostMessage);
|
|
214
|
+
setupCalComListener();
|
|
215
|
+
}
|
|
216
|
+
function stopCalendarTracking() {
|
|
217
|
+
if (!isListening) return;
|
|
218
|
+
window.removeEventListener("message", handlePostMessage);
|
|
219
|
+
callbacks = null;
|
|
220
|
+
isListening = false;
|
|
221
|
+
calCallbackRegistered = false;
|
|
222
|
+
calSetupAttempts = 0;
|
|
223
|
+
lastBookingUid = null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/session-tracker.ts
|
|
227
|
+
import { buildEngagementEvent } from "@outlit/core";
|
|
228
|
+
var DEFAULT_IDLE_TIMEOUT = 3e4;
|
|
229
|
+
var SESSION_TIMEOUT = 30 * 60 * 1e3;
|
|
230
|
+
var TIME_UPDATE_INTERVAL = 1e3;
|
|
231
|
+
var MIN_SPURIOUS_THRESHOLD = 50;
|
|
232
|
+
var SESSION_ID_KEY = "outlit_session_id";
|
|
233
|
+
var SESSION_LAST_ACTIVITY_KEY = "outlit_session_last_activity";
|
|
234
|
+
var SessionTracker = class {
|
|
235
|
+
state;
|
|
236
|
+
options;
|
|
237
|
+
idleTimeout;
|
|
238
|
+
timeUpdateInterval = null;
|
|
239
|
+
boundHandleActivity;
|
|
240
|
+
boundHandleVisibilityChange;
|
|
241
|
+
constructor(options) {
|
|
242
|
+
this.options = options;
|
|
243
|
+
this.idleTimeout = options.idleTimeout ?? DEFAULT_IDLE_TIMEOUT;
|
|
244
|
+
this.state = this.createInitialState();
|
|
245
|
+
this.boundHandleActivity = this.handleActivity.bind(this);
|
|
246
|
+
this.boundHandleVisibilityChange = this.handleVisibilityChange.bind(this);
|
|
247
|
+
this.setupEventListeners();
|
|
248
|
+
this.startTimeUpdateInterval();
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Get the current session ID.
|
|
252
|
+
*/
|
|
253
|
+
getSessionId() {
|
|
254
|
+
return this.state.sessionId;
|
|
255
|
+
}
|
|
256
|
+
// ============================================
|
|
257
|
+
// PUBLIC METHODS
|
|
258
|
+
// ============================================
|
|
259
|
+
/**
|
|
260
|
+
* Emit an engagement event for the current page session.
|
|
261
|
+
* Called by Tracker on exit events and SPA navigation.
|
|
262
|
+
*
|
|
263
|
+
* This method:
|
|
264
|
+
* 1. Finalizes any pending active time
|
|
265
|
+
* 2. Creates and emits the engagement event (if meaningful)
|
|
266
|
+
* 3. Resets state for the next session
|
|
267
|
+
*/
|
|
268
|
+
emitEngagement() {
|
|
269
|
+
if (this.state.hasEmittedEngagement) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
this.state.hasEmittedEngagement = true;
|
|
273
|
+
this.updateActiveTime();
|
|
274
|
+
const totalTimeMs = Date.now() - this.state.pageEntryTime;
|
|
275
|
+
const isSpuriousEvent = this.state.activeTimeMs < MIN_SPURIOUS_THRESHOLD && totalTimeMs < MIN_SPURIOUS_THRESHOLD;
|
|
276
|
+
if (!isSpuriousEvent) {
|
|
277
|
+
const event = buildEngagementEvent({
|
|
278
|
+
url: this.state.currentUrl,
|
|
279
|
+
referrer: document.referrer,
|
|
280
|
+
activeTimeMs: this.state.activeTimeMs,
|
|
281
|
+
totalTimeMs,
|
|
282
|
+
sessionId: this.state.sessionId
|
|
283
|
+
});
|
|
284
|
+
this.options.onEngagement(event);
|
|
285
|
+
}
|
|
286
|
+
this.resetState();
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Handle SPA navigation.
|
|
290
|
+
* Called by Tracker when a new pageview is detected.
|
|
291
|
+
*
|
|
292
|
+
* This method:
|
|
293
|
+
* 1. Emits engagement for the OLD page (using stored state)
|
|
294
|
+
* 2. Updates state for the NEW page
|
|
295
|
+
*/
|
|
296
|
+
onNavigation(newUrl) {
|
|
297
|
+
this.emitEngagement();
|
|
298
|
+
this.state.currentUrl = newUrl;
|
|
299
|
+
this.state.currentPath = this.extractPath(newUrl);
|
|
300
|
+
this.state.pageEntryTime = Date.now();
|
|
301
|
+
this.state.activeTimeMs = 0;
|
|
302
|
+
this.state.lastActiveTime = Date.now();
|
|
303
|
+
this.state.isUserActive = true;
|
|
304
|
+
this.state.hasEmittedEngagement = false;
|
|
305
|
+
this.resetIdleTimer();
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Stop session tracking and clean up.
|
|
309
|
+
*/
|
|
310
|
+
stop() {
|
|
311
|
+
this.removeEventListeners();
|
|
312
|
+
if (this.timeUpdateInterval) {
|
|
313
|
+
clearInterval(this.timeUpdateInterval);
|
|
314
|
+
this.timeUpdateInterval = null;
|
|
315
|
+
}
|
|
316
|
+
if (this.state.idleTimeoutId) {
|
|
317
|
+
clearTimeout(this.state.idleTimeoutId);
|
|
318
|
+
this.state.idleTimeoutId = null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// ============================================
|
|
322
|
+
// PRIVATE METHODS
|
|
323
|
+
// ============================================
|
|
324
|
+
createInitialState() {
|
|
325
|
+
const now = Date.now();
|
|
326
|
+
return {
|
|
327
|
+
currentUrl: typeof window !== "undefined" ? window.location.href : "",
|
|
328
|
+
currentPath: typeof window !== "undefined" ? window.location.pathname : "/",
|
|
329
|
+
pageEntryTime: now,
|
|
330
|
+
lastActiveTime: now,
|
|
331
|
+
activeTimeMs: 0,
|
|
332
|
+
isPageVisible: typeof document !== "undefined" ? document.visibilityState === "visible" : true,
|
|
333
|
+
isUserActive: true,
|
|
334
|
+
// Assume active on page load
|
|
335
|
+
idleTimeoutId: null,
|
|
336
|
+
sessionId: this.getOrCreateSessionId(),
|
|
337
|
+
hasEmittedEngagement: false
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
resetState() {
|
|
341
|
+
const now = Date.now();
|
|
342
|
+
this.state.pageEntryTime = now;
|
|
343
|
+
this.state.lastActiveTime = now;
|
|
344
|
+
this.state.activeTimeMs = 0;
|
|
345
|
+
this.state.isUserActive = true;
|
|
346
|
+
this.resetIdleTimer();
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Get existing session ID from storage or create a new one.
|
|
350
|
+
* Session ID is reset if:
|
|
351
|
+
* - No existing session ID in storage
|
|
352
|
+
* - Last activity was more than 30 minutes ago
|
|
353
|
+
*/
|
|
354
|
+
getOrCreateSessionId() {
|
|
355
|
+
if (typeof sessionStorage === "undefined") {
|
|
356
|
+
return this.generateSessionId();
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
const existingSessionId = sessionStorage.getItem(SESSION_ID_KEY);
|
|
360
|
+
const lastActivityStr = sessionStorage.getItem(SESSION_LAST_ACTIVITY_KEY);
|
|
361
|
+
const lastActivity = lastActivityStr ? Number.parseInt(lastActivityStr, 10) : 0;
|
|
362
|
+
const now = Date.now();
|
|
363
|
+
if (existingSessionId && lastActivity && now - lastActivity < SESSION_TIMEOUT) {
|
|
364
|
+
this.updateSessionActivity();
|
|
365
|
+
return existingSessionId;
|
|
366
|
+
}
|
|
367
|
+
const newSessionId = this.generateSessionId();
|
|
368
|
+
sessionStorage.setItem(SESSION_ID_KEY, newSessionId);
|
|
369
|
+
sessionStorage.setItem(SESSION_LAST_ACTIVITY_KEY, now.toString());
|
|
370
|
+
return newSessionId;
|
|
371
|
+
} catch {
|
|
372
|
+
return this.generateSessionId();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Generate a new session ID (UUID v4).
|
|
377
|
+
*/
|
|
378
|
+
generateSessionId() {
|
|
379
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
380
|
+
return crypto.randomUUID();
|
|
381
|
+
}
|
|
382
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
383
|
+
const r = Math.random() * 16 | 0;
|
|
384
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
385
|
+
return v.toString(16);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Update the session's last activity timestamp.
|
|
390
|
+
*/
|
|
391
|
+
updateSessionActivity() {
|
|
392
|
+
if (typeof sessionStorage === "undefined") return;
|
|
393
|
+
try {
|
|
394
|
+
sessionStorage.setItem(SESSION_LAST_ACTIVITY_KEY, Date.now().toString());
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Check if the current session has expired and create a new one if needed.
|
|
400
|
+
* Called when user returns to the page after being away.
|
|
401
|
+
*/
|
|
402
|
+
checkSessionExpiry() {
|
|
403
|
+
if (typeof sessionStorage === "undefined") return;
|
|
404
|
+
try {
|
|
405
|
+
const lastActivityStr = sessionStorage.getItem(SESSION_LAST_ACTIVITY_KEY);
|
|
406
|
+
const lastActivity = lastActivityStr ? Number.parseInt(lastActivityStr, 10) : 0;
|
|
407
|
+
const now = Date.now();
|
|
408
|
+
if (now - lastActivity >= SESSION_TIMEOUT) {
|
|
409
|
+
const newSessionId = this.generateSessionId();
|
|
410
|
+
sessionStorage.setItem(SESSION_ID_KEY, newSessionId);
|
|
411
|
+
this.state.sessionId = newSessionId;
|
|
412
|
+
}
|
|
413
|
+
sessionStorage.setItem(SESSION_LAST_ACTIVITY_KEY, now.toString());
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
setupEventListeners() {
|
|
418
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
419
|
+
const activityEvents = ["mousemove", "keydown", "click", "scroll", "touchstart"];
|
|
420
|
+
for (const event of activityEvents) {
|
|
421
|
+
document.addEventListener(event, this.boundHandleActivity, { passive: true });
|
|
422
|
+
}
|
|
423
|
+
document.addEventListener("visibilitychange", this.boundHandleVisibilityChange);
|
|
424
|
+
this.resetIdleTimer();
|
|
425
|
+
}
|
|
426
|
+
removeEventListeners() {
|
|
427
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
428
|
+
const activityEvents = ["mousemove", "keydown", "click", "scroll", "touchstart"];
|
|
429
|
+
for (const event of activityEvents) {
|
|
430
|
+
document.removeEventListener(event, this.boundHandleActivity);
|
|
431
|
+
}
|
|
432
|
+
document.removeEventListener("visibilitychange", this.boundHandleVisibilityChange);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Handle user activity events.
|
|
436
|
+
* Marks user as active and resets idle timer.
|
|
437
|
+
*/
|
|
438
|
+
handleActivity() {
|
|
439
|
+
if (!this.state.isUserActive) {
|
|
440
|
+
this.checkSessionExpiry();
|
|
441
|
+
this.state.lastActiveTime = Date.now();
|
|
442
|
+
}
|
|
443
|
+
this.state.isUserActive = true;
|
|
444
|
+
this.resetIdleTimer();
|
|
445
|
+
this.updateSessionActivity();
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Handle visibility change events.
|
|
449
|
+
* Pauses time accumulation when tab is hidden.
|
|
450
|
+
*/
|
|
451
|
+
handleVisibilityChange() {
|
|
452
|
+
const wasVisible = this.state.isPageVisible;
|
|
453
|
+
const isNowVisible = document.visibilityState === "visible";
|
|
454
|
+
if (wasVisible && !isNowVisible) {
|
|
455
|
+
this.updateActiveTime();
|
|
456
|
+
}
|
|
457
|
+
this.state.isPageVisible = isNowVisible;
|
|
458
|
+
if (!wasVisible && isNowVisible) {
|
|
459
|
+
this.checkSessionExpiry();
|
|
460
|
+
this.state.lastActiveTime = Date.now();
|
|
461
|
+
this.state.hasEmittedEngagement = false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Reset the idle timer.
|
|
466
|
+
* Called on activity and initialization.
|
|
467
|
+
*/
|
|
468
|
+
resetIdleTimer() {
|
|
469
|
+
if (this.state.idleTimeoutId) {
|
|
470
|
+
clearTimeout(this.state.idleTimeoutId);
|
|
471
|
+
}
|
|
472
|
+
this.state.idleTimeoutId = setTimeout(() => {
|
|
473
|
+
this.updateActiveTime();
|
|
474
|
+
this.state.isUserActive = false;
|
|
475
|
+
}, this.idleTimeout);
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Start the interval for updating active time.
|
|
479
|
+
*/
|
|
480
|
+
startTimeUpdateInterval() {
|
|
481
|
+
if (this.timeUpdateInterval) return;
|
|
482
|
+
this.timeUpdateInterval = setInterval(() => {
|
|
483
|
+
this.updateActiveTime();
|
|
484
|
+
}, TIME_UPDATE_INTERVAL);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Update accumulated active time.
|
|
488
|
+
* Only accumulates when page is visible AND user is active.
|
|
489
|
+
*/
|
|
490
|
+
updateActiveTime() {
|
|
491
|
+
if (this.state.isPageVisible && this.state.isUserActive) {
|
|
492
|
+
const now = Date.now();
|
|
493
|
+
this.state.activeTimeMs += now - this.state.lastActiveTime;
|
|
494
|
+
this.state.lastActiveTime = now;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Extract path from URL.
|
|
499
|
+
*/
|
|
500
|
+
extractPath(url) {
|
|
501
|
+
try {
|
|
502
|
+
return new URL(url).pathname;
|
|
503
|
+
} catch {
|
|
504
|
+
return "/";
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
var sessionTrackerInstance = null;
|
|
509
|
+
function initSessionTracking(options) {
|
|
510
|
+
if (sessionTrackerInstance) {
|
|
511
|
+
console.warn("[Outlit] Session tracking already initialized");
|
|
512
|
+
return sessionTrackerInstance;
|
|
513
|
+
}
|
|
514
|
+
sessionTrackerInstance = new SessionTracker(options);
|
|
515
|
+
return sessionTrackerInstance;
|
|
516
|
+
}
|
|
517
|
+
function stopSessionTracking() {
|
|
518
|
+
if (sessionTrackerInstance) {
|
|
519
|
+
sessionTrackerInstance.stop();
|
|
520
|
+
sessionTrackerInstance = null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
95
524
|
// src/storage.ts
|
|
96
525
|
var VISITOR_ID_KEY = "outlit_visitor_id";
|
|
97
526
|
function generateVisitorId() {
|
|
@@ -173,7 +602,7 @@ function setCookie(name, value, days) {
|
|
|
173
602
|
}
|
|
174
603
|
|
|
175
604
|
// src/tracker.ts
|
|
176
|
-
var
|
|
605
|
+
var Outlit = class {
|
|
177
606
|
publicKey;
|
|
178
607
|
apiHost;
|
|
179
608
|
visitorId = null;
|
|
@@ -183,15 +612,29 @@ var Tracker = class {
|
|
|
183
612
|
isInitialized = false;
|
|
184
613
|
isTrackingEnabled = false;
|
|
185
614
|
options;
|
|
615
|
+
hasHandledExit = false;
|
|
616
|
+
sessionTracker = null;
|
|
186
617
|
constructor(options) {
|
|
187
618
|
this.publicKey = options.publicKey;
|
|
188
619
|
this.apiHost = options.apiHost ?? DEFAULT_API_HOST;
|
|
189
620
|
this.flushInterval = options.flushInterval ?? 5e3;
|
|
190
621
|
this.options = options;
|
|
191
622
|
if (typeof window !== "undefined") {
|
|
192
|
-
|
|
623
|
+
const handleExit = () => {
|
|
624
|
+
if (this.hasHandledExit) return;
|
|
625
|
+
this.hasHandledExit = true;
|
|
626
|
+
this.sessionTracker?.emitEngagement();
|
|
193
627
|
this.flush();
|
|
628
|
+
};
|
|
629
|
+
document.addEventListener("visibilitychange", () => {
|
|
630
|
+
if (document.visibilityState === "hidden") {
|
|
631
|
+
handleExit();
|
|
632
|
+
} else {
|
|
633
|
+
this.hasHandledExit = false;
|
|
634
|
+
}
|
|
194
635
|
});
|
|
636
|
+
window.addEventListener("pagehide", handleExit);
|
|
637
|
+
window.addEventListener("beforeunload", handleExit);
|
|
195
638
|
}
|
|
196
639
|
this.isInitialized = true;
|
|
197
640
|
if (options.autoTrack !== false) {
|
|
@@ -216,12 +659,18 @@ var Tracker = class {
|
|
|
216
659
|
}
|
|
217
660
|
this.visitorId = getOrCreateVisitorId();
|
|
218
661
|
this.startFlushTimer();
|
|
662
|
+
if (this.options.trackEngagement !== false) {
|
|
663
|
+
this.initSessionTracking();
|
|
664
|
+
}
|
|
219
665
|
if (this.options.trackPageviews !== false) {
|
|
220
666
|
this.initPageviewTracking();
|
|
221
667
|
}
|
|
222
668
|
if (this.options.trackForms !== false) {
|
|
223
669
|
this.initFormTracking(this.options.formFieldDenylist);
|
|
224
670
|
}
|
|
671
|
+
if (this.options.trackCalendarEmbeds !== false) {
|
|
672
|
+
this.initCalendarTracking();
|
|
673
|
+
}
|
|
225
674
|
this.isTrackingEnabled = true;
|
|
226
675
|
}
|
|
227
676
|
/**
|
|
@@ -281,7 +730,7 @@ var Tracker = class {
|
|
|
281
730
|
await this.sendEvents(events);
|
|
282
731
|
}
|
|
283
732
|
/**
|
|
284
|
-
* Shutdown the
|
|
733
|
+
* Shutdown the client.
|
|
285
734
|
*/
|
|
286
735
|
async shutdown() {
|
|
287
736
|
if (this.flushTimer) {
|
|
@@ -289,13 +738,25 @@ var Tracker = class {
|
|
|
289
738
|
this.flushTimer = null;
|
|
290
739
|
}
|
|
291
740
|
stopAutocapture();
|
|
741
|
+
stopCalendarTracking();
|
|
742
|
+
stopSessionTracking();
|
|
743
|
+
this.sessionTracker = null;
|
|
292
744
|
await this.flush();
|
|
293
745
|
}
|
|
294
746
|
// ============================================
|
|
295
747
|
// INTERNAL METHODS
|
|
296
748
|
// ============================================
|
|
749
|
+
initSessionTracking() {
|
|
750
|
+
this.sessionTracker = initSessionTracking({
|
|
751
|
+
onEngagement: (event) => {
|
|
752
|
+
this.enqueue(event);
|
|
753
|
+
},
|
|
754
|
+
idleTimeout: this.options.idleTimeout
|
|
755
|
+
});
|
|
756
|
+
}
|
|
297
757
|
initPageviewTracking() {
|
|
298
758
|
initPageviewTracking((url, referrer, title) => {
|
|
759
|
+
this.sessionTracker?.onNavigation(url);
|
|
299
760
|
const event = buildPageviewEvent({ url, referrer, title });
|
|
300
761
|
this.enqueue(event);
|
|
301
762
|
});
|
|
@@ -325,6 +786,25 @@ var Tracker = class {
|
|
|
325
786
|
identityCallback2
|
|
326
787
|
);
|
|
327
788
|
}
|
|
789
|
+
initCalendarTracking() {
|
|
790
|
+
initCalendarTracking({
|
|
791
|
+
onCalendarBooked: (bookingEvent) => {
|
|
792
|
+
const event = buildCalendarEvent({
|
|
793
|
+
url: window.location.href,
|
|
794
|
+
referrer: document.referrer,
|
|
795
|
+
provider: bookingEvent.provider,
|
|
796
|
+
eventType: bookingEvent.eventType,
|
|
797
|
+
startTime: bookingEvent.startTime,
|
|
798
|
+
endTime: bookingEvent.endTime,
|
|
799
|
+
duration: bookingEvent.duration,
|
|
800
|
+
isRecurring: bookingEvent.isRecurring,
|
|
801
|
+
inviteeEmail: bookingEvent.inviteeEmail,
|
|
802
|
+
inviteeName: bookingEvent.inviteeName
|
|
803
|
+
});
|
|
804
|
+
this.enqueue(event);
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
}
|
|
328
808
|
enqueue(event) {
|
|
329
809
|
this.eventQueue.push(event);
|
|
330
810
|
if (this.eventQueue.length >= 10) {
|
|
@@ -340,7 +820,7 @@ var Tracker = class {
|
|
|
340
820
|
async sendEvents(events) {
|
|
341
821
|
if (events.length === 0) return;
|
|
342
822
|
if (!this.visitorId) return;
|
|
343
|
-
const payload = buildIngestPayload(this.visitorId, "
|
|
823
|
+
const payload = buildIngestPayload(this.visitorId, "client", events);
|
|
344
824
|
const url = `${this.apiHost}/api/i/v1/${this.publicKey}/events`;
|
|
345
825
|
try {
|
|
346
826
|
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
@@ -365,7 +845,7 @@ var Tracker = class {
|
|
|
365
845
|
// src/react/provider.tsx
|
|
366
846
|
import { jsx } from "react/jsx-runtime";
|
|
367
847
|
var OutlitContext = createContext({
|
|
368
|
-
|
|
848
|
+
outlit: null,
|
|
369
849
|
isInitialized: false,
|
|
370
850
|
isTrackingEnabled: false,
|
|
371
851
|
enableTracking: () => {
|
|
@@ -382,12 +862,12 @@ function OutlitProvider({
|
|
|
382
862
|
autoTrack = true,
|
|
383
863
|
autoIdentify = true
|
|
384
864
|
}) {
|
|
385
|
-
const
|
|
865
|
+
const outlitRef = useRef(null);
|
|
386
866
|
const initializedRef = useRef(false);
|
|
387
867
|
const [isTrackingEnabled, setIsTrackingEnabled] = useState(false);
|
|
388
868
|
useEffect(() => {
|
|
389
869
|
if (initializedRef.current) return;
|
|
390
|
-
|
|
870
|
+
outlitRef.current = new Outlit({
|
|
391
871
|
publicKey,
|
|
392
872
|
apiHost,
|
|
393
873
|
trackPageviews,
|
|
@@ -398,9 +878,9 @@ function OutlitProvider({
|
|
|
398
878
|
autoIdentify
|
|
399
879
|
});
|
|
400
880
|
initializedRef.current = true;
|
|
401
|
-
setIsTrackingEnabled(
|
|
881
|
+
setIsTrackingEnabled(outlitRef.current.isEnabled());
|
|
402
882
|
return () => {
|
|
403
|
-
|
|
883
|
+
outlitRef.current?.shutdown();
|
|
404
884
|
};
|
|
405
885
|
}, [
|
|
406
886
|
publicKey,
|
|
@@ -413,8 +893,8 @@ function OutlitProvider({
|
|
|
413
893
|
autoIdentify
|
|
414
894
|
]);
|
|
415
895
|
const enableTracking = useCallback(() => {
|
|
416
|
-
if (
|
|
417
|
-
|
|
896
|
+
if (outlitRef.current) {
|
|
897
|
+
outlitRef.current.enableTracking();
|
|
418
898
|
setIsTrackingEnabled(true);
|
|
419
899
|
}
|
|
420
900
|
}, []);
|
|
@@ -422,7 +902,7 @@ function OutlitProvider({
|
|
|
422
902
|
OutlitContext.Provider,
|
|
423
903
|
{
|
|
424
904
|
value: {
|
|
425
|
-
|
|
905
|
+
outlit: outlitRef.current,
|
|
426
906
|
isInitialized: initializedRef.current,
|
|
427
907
|
isTrackingEnabled,
|
|
428
908
|
enableTracking
|
|
@@ -435,31 +915,31 @@ function OutlitProvider({
|
|
|
435
915
|
// src/react/hooks.ts
|
|
436
916
|
import { useCallback as useCallback2, useContext } from "react";
|
|
437
917
|
function useOutlit() {
|
|
438
|
-
const {
|
|
918
|
+
const { outlit, isInitialized, isTrackingEnabled, enableTracking } = useContext(OutlitContext);
|
|
439
919
|
const track = useCallback2(
|
|
440
920
|
(eventName, properties) => {
|
|
441
|
-
if (!
|
|
442
|
-
console.warn("[Outlit]
|
|
921
|
+
if (!outlit) {
|
|
922
|
+
console.warn("[Outlit] Not initialized. Make sure OutlitProvider is mounted.");
|
|
443
923
|
return;
|
|
444
924
|
}
|
|
445
|
-
|
|
925
|
+
outlit.track(eventName, properties);
|
|
446
926
|
},
|
|
447
|
-
[
|
|
927
|
+
[outlit]
|
|
448
928
|
);
|
|
449
929
|
const identify = useCallback2(
|
|
450
930
|
(options) => {
|
|
451
|
-
if (!
|
|
452
|
-
console.warn("[Outlit]
|
|
931
|
+
if (!outlit) {
|
|
932
|
+
console.warn("[Outlit] Not initialized. Make sure OutlitProvider is mounted.");
|
|
453
933
|
return;
|
|
454
934
|
}
|
|
455
|
-
|
|
935
|
+
outlit.identify(options);
|
|
456
936
|
},
|
|
457
|
-
[
|
|
937
|
+
[outlit]
|
|
458
938
|
);
|
|
459
939
|
const getVisitorId = useCallback2(() => {
|
|
460
|
-
if (!
|
|
461
|
-
return
|
|
462
|
-
}, [
|
|
940
|
+
if (!outlit) return null;
|
|
941
|
+
return outlit.getVisitorId();
|
|
942
|
+
}, [outlit]);
|
|
463
943
|
return {
|
|
464
944
|
track,
|
|
465
945
|
identify,
|