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