weathervane 0.1.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/README.md +471 -0
- package/package.json +29 -0
- package/weathervane.js +1026 -0
- package/weathervane.min.js +16 -0
- package/weathervane.min.js.gz +0 -0
package/weathervane.js
ADDED
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Weathervane v0.1.0
|
|
3
|
+
* A lightweight, zero-backend analytics utility.
|
|
4
|
+
*
|
|
5
|
+
* Weathervane does NOT send data anywhere. It observes user behavior
|
|
6
|
+
* (pageviews, content exposure, clicks, forms, sessions, web vitals)
|
|
7
|
+
* and emits structured browser CustomEvents that you can forward to
|
|
8
|
+
* any destination (GA4, PostHog, Segment, your own API, ...).
|
|
9
|
+
*
|
|
10
|
+
* window.addEventListener('vane:event', function (e) {
|
|
11
|
+
* console.log(e.detail.event_name, e.detail);
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* License: MIT
|
|
15
|
+
*/
|
|
16
|
+
(function (window, document) {
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
if (!window || !document) return;
|
|
20
|
+
if (window.vane && window.vane.__loaded) return;
|
|
21
|
+
|
|
22
|
+
var VERSION = '0.1.0';
|
|
23
|
+
var existing = window.vane; // may hold a _queue from an async loader snippet
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------
|
|
26
|
+
// Configuration
|
|
27
|
+
// ---------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
var DEFAULTS = {
|
|
30
|
+
// Emission
|
|
31
|
+
eventPrefix: 'vane', // events fire as `${prefix}:event` and `${prefix}:${event_name}`
|
|
32
|
+
historySize: 100, // recent events kept for on(..., { replay: true })
|
|
33
|
+
|
|
34
|
+
// Feature toggles
|
|
35
|
+
enableAutoPageview: true, // emit `pageview` on DOM ready
|
|
36
|
+
enableDynamicPageview: true, // emit `pageview_dynamic` on SPA navigation
|
|
37
|
+
enableContentTracking: true, // data-vane-content lifecycle tracking
|
|
38
|
+
enableLinkTracking: true, // emit `link_click` for every <a href> click
|
|
39
|
+
enableFormTracking: true, // emit `form_submit` / `form_abandon`
|
|
40
|
+
enableWebVitals: true, // collect FCP / LCP / CLS / FID
|
|
41
|
+
trackShadowDom: true, // scan open shadow roots, retarget composed events
|
|
42
|
+
|
|
43
|
+
// Session management
|
|
44
|
+
sessionTimeout: 30, // minutes of inactivity before a new session
|
|
45
|
+
|
|
46
|
+
// Content tracking
|
|
47
|
+
contentExposureLimit: 1000, // default ms in viewport before `content_view`
|
|
48
|
+
largeContentViewportFill: 0.65, // taller-than-viewport content counts as visible
|
|
49
|
+
// when it fills this fraction of the viewport
|
|
50
|
+
|
|
51
|
+
// Form tracking
|
|
52
|
+
formAbandonThreshold: 3000, // min ms of engagement before `form_abandon` fires
|
|
53
|
+
|
|
54
|
+
// Development
|
|
55
|
+
debug: false
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
var config = assign({}, DEFAULTS);
|
|
59
|
+
var ready = false;
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------
|
|
62
|
+
// Small utilities
|
|
63
|
+
// ---------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
function assign(target) {
|
|
66
|
+
for (var i = 1; i < arguments.length; i++) {
|
|
67
|
+
var src = arguments[i];
|
|
68
|
+
if (!src) continue;
|
|
69
|
+
for (var key in src) {
|
|
70
|
+
if (Object.prototype.hasOwnProperty.call(src, key)) target[key] = src[key];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return target;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function now() { return Date.now(); }
|
|
77
|
+
|
|
78
|
+
function randomBytes(length) {
|
|
79
|
+
var bytes = new Uint8Array(length);
|
|
80
|
+
if (window.crypto && window.crypto.getRandomValues) {
|
|
81
|
+
window.crypto.getRandomValues(bytes);
|
|
82
|
+
} else {
|
|
83
|
+
for (var i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
84
|
+
}
|
|
85
|
+
return bytes;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// RFC 4122 v4 UUID — used for client / session / page-view / instance ids
|
|
89
|
+
function uuid() {
|
|
90
|
+
if (window.crypto && window.crypto.randomUUID) {
|
|
91
|
+
try { return window.crypto.randomUUID(); } catch (e) { /* insecure context */ }
|
|
92
|
+
}
|
|
93
|
+
var b = randomBytes(16);
|
|
94
|
+
b[6] = (b[6] & 0x0f) | 0x40;
|
|
95
|
+
b[8] = (b[8] & 0x3f) | 0x80;
|
|
96
|
+
var hex = '';
|
|
97
|
+
for (var i = 0; i < 16; i++) hex += (b[i] + 0x100).toString(16).slice(1);
|
|
98
|
+
return hex.slice(0, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16) +
|
|
99
|
+
'-' + hex.slice(16, 20) + '-' + hex.slice(20);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ULID — lexicographically sortable event ids for time-series friendliness
|
|
103
|
+
var ULID_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
104
|
+
function ulid() {
|
|
105
|
+
var ts = now();
|
|
106
|
+
var out = '';
|
|
107
|
+
for (var i = 0; i < 10; i++) {
|
|
108
|
+
out = ULID_CHARS[ts % 32] + out;
|
|
109
|
+
ts = Math.floor(ts / 32);
|
|
110
|
+
}
|
|
111
|
+
var rand = randomBytes(16);
|
|
112
|
+
for (var j = 0; j < 16; j++) out += ULID_CHARS[rand[j] % 32];
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function truncate(str, max) {
|
|
117
|
+
str = (str || '').replace(/\s+/g, ' ').trim();
|
|
118
|
+
return str.length > max ? str.slice(0, max) : str;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function log() {
|
|
122
|
+
if (!config.debug || !window.console) return;
|
|
123
|
+
var args = ['%c vane ', 'background:#5b5bd6;color:#fff;border-radius:3px'];
|
|
124
|
+
for (var i = 0; i < arguments.length; i++) args.push(arguments[i]);
|
|
125
|
+
console.log.apply(console, args);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Listener bookkeeping so destroy() can tear everything down
|
|
129
|
+
var cleanups = [];
|
|
130
|
+
function listen(target, type, handler, options) {
|
|
131
|
+
target.addEventListener(type, handler, options);
|
|
132
|
+
cleanups.push(function () { target.removeEventListener(type, handler, options); });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function whenDomReady(fn) {
|
|
136
|
+
if (document.readyState === 'loading') {
|
|
137
|
+
listen(document, 'DOMContentLoaded', fn);
|
|
138
|
+
} else {
|
|
139
|
+
fn();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------
|
|
144
|
+
// Storage (localStorage with in-memory fallback — no cookies, ever)
|
|
145
|
+
// ---------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
var memoryStore = {};
|
|
148
|
+
|
|
149
|
+
function storageGet(key) {
|
|
150
|
+
try {
|
|
151
|
+
var value = window.localStorage.getItem(key);
|
|
152
|
+
if (value !== null) return value;
|
|
153
|
+
} catch (e) { /* unavailable or blocked */ }
|
|
154
|
+
return Object.prototype.hasOwnProperty.call(memoryStore, key) ? memoryStore[key] : null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function storageSet(key, value) {
|
|
158
|
+
memoryStore[key] = value;
|
|
159
|
+
try { window.localStorage.setItem(key, value); } catch (e) { /* memory fallback only */ }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------
|
|
163
|
+
// Identity & session
|
|
164
|
+
// ---------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
var CID_KEY = 'vane_cid';
|
|
167
|
+
var SID_KEY = 'vane_sid';
|
|
168
|
+
|
|
169
|
+
var clientId = null;
|
|
170
|
+
var session = { id: null, lastActive: 0, lastPersist: 0 };
|
|
171
|
+
var userId = null;
|
|
172
|
+
var pageViewId = null;
|
|
173
|
+
var pageStartTime = now();
|
|
174
|
+
var context = {};
|
|
175
|
+
|
|
176
|
+
function loadClientId() {
|
|
177
|
+
clientId = storageGet(CID_KEY);
|
|
178
|
+
if (!clientId) clientId = uuid();
|
|
179
|
+
storageSet(CID_KEY, clientId);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function persistSession() {
|
|
183
|
+
storageSet(SID_KEY, session.id + '.' + session.lastActive);
|
|
184
|
+
session.lastPersist = now();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function loadSession() {
|
|
188
|
+
var raw = storageGet(SID_KEY);
|
|
189
|
+
var sid = null;
|
|
190
|
+
var lastActive = 0;
|
|
191
|
+
if (raw) {
|
|
192
|
+
var parts = raw.split('.');
|
|
193
|
+
sid = parts[0] || null;
|
|
194
|
+
lastActive = parseInt(parts[1], 10) || 0;
|
|
195
|
+
}
|
|
196
|
+
var expired = !sid || (now() - lastActive) > config.sessionTimeout * 60000;
|
|
197
|
+
if (expired) {
|
|
198
|
+
startNewSession(sid ? 'timeout' : 'new', sid);
|
|
199
|
+
} else {
|
|
200
|
+
session.id = sid;
|
|
201
|
+
session.lastActive = now();
|
|
202
|
+
persistSession();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function startNewSession(reason, previousId) {
|
|
207
|
+
var prev = previousId !== undefined ? previousId : session.id;
|
|
208
|
+
session.id = uuid();
|
|
209
|
+
session.lastActive = now();
|
|
210
|
+
persistSession();
|
|
211
|
+
emit('session_start', { reason: reason, previous_session_id: prev || null });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function touchSession() {
|
|
215
|
+
var t = now();
|
|
216
|
+
if (t - session.lastActive > config.sessionTimeout * 60000) {
|
|
217
|
+
startNewSession('timeout');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
session.lastActive = t;
|
|
221
|
+
if (t - session.lastPersist > 10000) persistSession();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------
|
|
225
|
+
// Context collection (page, device, UTM, performance)
|
|
226
|
+
// ---------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
function pageContext() {
|
|
229
|
+
var loc = window.location;
|
|
230
|
+
return {
|
|
231
|
+
url: loc.href,
|
|
232
|
+
path: loc.pathname,
|
|
233
|
+
title: document.title || null,
|
|
234
|
+
referrer: document.referrer || null,
|
|
235
|
+
search: loc.search || null,
|
|
236
|
+
hash: loc.hash || null
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function parseBrowser(ua) {
|
|
241
|
+
var match;
|
|
242
|
+
if ((match = ua.match(/Edg(?:e|A|iOS)?\/([\d.]+)/))) return { name: 'Edge', version: match[1] };
|
|
243
|
+
if ((match = ua.match(/OPR\/([\d.]+)/))) return { name: 'Opera', version: match[1] };
|
|
244
|
+
if ((match = ua.match(/SamsungBrowser\/([\d.]+)/))) return { name: 'Samsung Internet', version: match[1] };
|
|
245
|
+
if ((match = ua.match(/(?:Firefox|FxiOS)\/([\d.]+)/))) return { name: 'Firefox', version: match[1] };
|
|
246
|
+
if ((match = ua.match(/(?:Chrome|CriOS)\/([\d.]+)/))) return { name: 'Chrome', version: match[1] };
|
|
247
|
+
if (/Safari\//.test(ua) && (match = ua.match(/Version\/([\d.]+)/))) return { name: 'Safari', version: match[1] };
|
|
248
|
+
return { name: 'unknown', version: null };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function detectDeviceType(ua) {
|
|
252
|
+
if (/iPad|Tablet|PlayBook/i.test(ua) || (/Android/i.test(ua) && !/Mobile/i.test(ua))) return 'tablet';
|
|
253
|
+
if (/Mobi|iPhone|iPod|Android/i.test(ua)) return 'mobile';
|
|
254
|
+
return 'desktop';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
var staticDevice = null;
|
|
258
|
+
function deviceContext() {
|
|
259
|
+
if (!staticDevice) {
|
|
260
|
+
var ua = navigator.userAgent || '';
|
|
261
|
+
var browser = parseBrowser(ua);
|
|
262
|
+
var timezone = null;
|
|
263
|
+
try { timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (e) { /* no Intl */ }
|
|
264
|
+
staticDevice = {
|
|
265
|
+
user_agent: ua,
|
|
266
|
+
browser_name: browser.name,
|
|
267
|
+
browser_version: browser.version,
|
|
268
|
+
language: navigator.language || null,
|
|
269
|
+
timezone: timezone,
|
|
270
|
+
screen_width: window.screen ? window.screen.width : null,
|
|
271
|
+
screen_height: window.screen ? window.screen.height : null,
|
|
272
|
+
device_type: detectDeviceType(ua),
|
|
273
|
+
device_memory: navigator.deviceMemory || null,
|
|
274
|
+
hardware_concurrency: navigator.hardwareConcurrency || null,
|
|
275
|
+
cookie_enabled: !!navigator.cookieEnabled
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return assign({}, staticDevice, {
|
|
279
|
+
viewport_width: window.innerWidth,
|
|
280
|
+
viewport_height: window.innerHeight,
|
|
281
|
+
online: navigator.onLine !== false
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
var currentUtm = null;
|
|
286
|
+
function parseUtm() {
|
|
287
|
+
var out = {};
|
|
288
|
+
var found = false;
|
|
289
|
+
try {
|
|
290
|
+
var params = new URLSearchParams(window.location.search);
|
|
291
|
+
var keys = ['source', 'medium', 'campaign', 'term', 'content'];
|
|
292
|
+
for (var i = 0; i < keys.length; i++) {
|
|
293
|
+
var value = params.get('utm_' + keys[i]);
|
|
294
|
+
if (value) { out['utm_' + keys[i]] = value; found = true; }
|
|
295
|
+
}
|
|
296
|
+
} catch (e) { /* URLSearchParams unavailable */ }
|
|
297
|
+
return found ? out : null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function navTiming() {
|
|
301
|
+
try {
|
|
302
|
+
var nav = performance.getEntriesByType('navigation')[0];
|
|
303
|
+
if (!nav) return {};
|
|
304
|
+
return {
|
|
305
|
+
page_load_time: nav.loadEventEnd > 0 ? Math.round(nav.loadEventEnd) : null,
|
|
306
|
+
dom_content_loaded_time: nav.domContentLoadedEventEnd > 0 ? Math.round(nav.domContentLoadedEventEnd) : null,
|
|
307
|
+
navigation_type: nav.type || null
|
|
308
|
+
};
|
|
309
|
+
} catch (e) { return {}; }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function connectionInfo() {
|
|
313
|
+
var conn = navigator.connection;
|
|
314
|
+
if (!conn) return {};
|
|
315
|
+
return {
|
|
316
|
+
connection_type: conn.effectiveType || null,
|
|
317
|
+
connection_speed: typeof conn.downlink === 'number' ? conn.downlink : null
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---------------------------------------------------------------------
|
|
322
|
+
// Web vitals (FCP, LCP, CLS, FID)
|
|
323
|
+
// ---------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
var vitals = {
|
|
326
|
+
first_contentful_paint: null,
|
|
327
|
+
largest_contentful_paint: null,
|
|
328
|
+
cumulative_layout_shift: null,
|
|
329
|
+
first_input_delay: null
|
|
330
|
+
};
|
|
331
|
+
var vitalsObservers = [];
|
|
332
|
+
|
|
333
|
+
function observeVitals() {
|
|
334
|
+
if (!config.enableWebVitals || typeof PerformanceObserver === 'undefined') return;
|
|
335
|
+
|
|
336
|
+
function observe(type, handler) {
|
|
337
|
+
try {
|
|
338
|
+
var observer = new PerformanceObserver(function (list) {
|
|
339
|
+
handler(list.getEntries());
|
|
340
|
+
});
|
|
341
|
+
observer.observe({ type: type, buffered: true });
|
|
342
|
+
vitalsObservers.push(observer);
|
|
343
|
+
} catch (e) { /* entry type unsupported */ }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
observe('paint', function (entries) {
|
|
347
|
+
for (var i = 0; i < entries.length; i++) {
|
|
348
|
+
if (entries[i].name === 'first-contentful-paint') {
|
|
349
|
+
vitals.first_contentful_paint = Math.round(entries[i].startTime);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
observe('largest-contentful-paint', function (entries) {
|
|
354
|
+
var last = entries[entries.length - 1];
|
|
355
|
+
if (last) vitals.largest_contentful_paint = Math.round(last.startTime);
|
|
356
|
+
});
|
|
357
|
+
observe('layout-shift', function (entries) {
|
|
358
|
+
for (var i = 0; i < entries.length; i++) {
|
|
359
|
+
if (!entries[i].hadRecentInput) {
|
|
360
|
+
vitals.cumulative_layout_shift =
|
|
361
|
+
Math.round(((vitals.cumulative_layout_shift || 0) + entries[i].value) * 10000) / 10000;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
observe('first-input', function (entries) {
|
|
366
|
+
var first = entries[0];
|
|
367
|
+
if (first && vitals.first_input_delay === null) {
|
|
368
|
+
vitals.first_input_delay = Math.round(first.processingStart - first.startTime);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ---------------------------------------------------------------------
|
|
374
|
+
// Scroll depth
|
|
375
|
+
// ---------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
var scrollDepth = 0;
|
|
378
|
+
var scrollTickPending = false;
|
|
379
|
+
|
|
380
|
+
function updateScrollDepth() {
|
|
381
|
+
scrollTickPending = false;
|
|
382
|
+
var doc = document.documentElement;
|
|
383
|
+
var scrollable = doc.scrollHeight - window.innerHeight;
|
|
384
|
+
var pct = scrollable > 0
|
|
385
|
+
? Math.min(100, Math.round((window.scrollY / scrollable) * 100))
|
|
386
|
+
: 100;
|
|
387
|
+
if (pct > scrollDepth) scrollDepth = pct;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function observeScroll() {
|
|
391
|
+
listen(window, 'scroll', function () {
|
|
392
|
+
if (scrollTickPending) return;
|
|
393
|
+
scrollTickPending = true;
|
|
394
|
+
window.requestAnimationFrame(updateScrollDepth);
|
|
395
|
+
}, { passive: true });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ---------------------------------------------------------------------
|
|
399
|
+
// Event emission
|
|
400
|
+
// ---------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
var eventHistory = [];
|
|
403
|
+
var pendingEvents = []; // track() calls made before init()
|
|
404
|
+
|
|
405
|
+
function buildPayload(eventName, properties) {
|
|
406
|
+
return {
|
|
407
|
+
event_id: ulid(),
|
|
408
|
+
event_name: eventName,
|
|
409
|
+
timestamp: new Date().toISOString(),
|
|
410
|
+
client_id: clientId,
|
|
411
|
+
session_id: session.id,
|
|
412
|
+
page_view_id: pageViewId,
|
|
413
|
+
user_id: userId,
|
|
414
|
+
properties: properties || {},
|
|
415
|
+
page: pageContext(),
|
|
416
|
+
device: deviceContext(),
|
|
417
|
+
utm: currentUtm,
|
|
418
|
+
performance: assign({}, vitals),
|
|
419
|
+
engagement: {
|
|
420
|
+
scroll_depth: scrollDepth,
|
|
421
|
+
time_on_page: now() - pageStartTime
|
|
422
|
+
},
|
|
423
|
+
context: assign({}, context),
|
|
424
|
+
sdk: { name: 'weathervane', version: VERSION }
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function dispatch(type, payload) {
|
|
429
|
+
var event;
|
|
430
|
+
try {
|
|
431
|
+
event = new CustomEvent(type, { detail: payload });
|
|
432
|
+
} catch (e) {
|
|
433
|
+
event = document.createEvent('CustomEvent');
|
|
434
|
+
event.initCustomEvent(type, false, false, payload);
|
|
435
|
+
}
|
|
436
|
+
window.dispatchEvent(event);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function emit(eventName, properties) {
|
|
440
|
+
if (!ready) {
|
|
441
|
+
pendingEvents.push([eventName, properties]);
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
var payload = buildPayload(eventName, properties);
|
|
445
|
+
eventHistory.push(payload);
|
|
446
|
+
if (eventHistory.length > config.historySize) {
|
|
447
|
+
eventHistory.splice(0, eventHistory.length - config.historySize);
|
|
448
|
+
}
|
|
449
|
+
dispatch(config.eventPrefix + ':event', payload);
|
|
450
|
+
dispatch(config.eventPrefix + ':' + eventName, payload);
|
|
451
|
+
log(eventName, payload);
|
|
452
|
+
return payload;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ---------------------------------------------------------------------
|
|
456
|
+
// Pageviews & SPA navigation
|
|
457
|
+
// ---------------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
var lastUrl = window.location.href;
|
|
460
|
+
var historyPatched = false;
|
|
461
|
+
|
|
462
|
+
function startPageView(eventName, extraProps) {
|
|
463
|
+
pageViewId = uuid();
|
|
464
|
+
pageStartTime = now();
|
|
465
|
+
scrollDepth = 0;
|
|
466
|
+
currentUtm = parseUtm();
|
|
467
|
+
emit(eventName, assign({}, extraProps));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function onNavigate(trigger) {
|
|
471
|
+
if (window.location.href === lastUrl) return;
|
|
472
|
+
lastUrl = window.location.href;
|
|
473
|
+
touchSession();
|
|
474
|
+
flushFormAbandons();
|
|
475
|
+
if (!config.enableDynamicPageview) return;
|
|
476
|
+
var props = { navigation_trigger: trigger };
|
|
477
|
+
if (trigger === 'hashchange') props.new_hash = window.location.hash || null;
|
|
478
|
+
startPageView('pageview_dynamic', props);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function patchHistory() {
|
|
482
|
+
if (historyPatched || !window.history || !window.history.pushState) return;
|
|
483
|
+
historyPatched = true;
|
|
484
|
+
['pushState', 'replaceState'].forEach(function (method) {
|
|
485
|
+
var original = window.history[method];
|
|
486
|
+
window.history[method] = function () {
|
|
487
|
+
var result = original.apply(this, arguments);
|
|
488
|
+
onNavigate(method);
|
|
489
|
+
return result;
|
|
490
|
+
};
|
|
491
|
+
cleanups.push(function () { window.history[method] = original; });
|
|
492
|
+
});
|
|
493
|
+
listen(window, 'popstate', function () { onNavigate('popstate'); });
|
|
494
|
+
listen(window, 'hashchange', function () { onNavigate('hashchange'); });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ---------------------------------------------------------------------
|
|
498
|
+
// Content tracking (data-vane-content)
|
|
499
|
+
// ---------------------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
var contentStates = new Map();
|
|
502
|
+
var intersectionObserver = null;
|
|
503
|
+
var mutationObserver = null;
|
|
504
|
+
|
|
505
|
+
function contentDepth(el) {
|
|
506
|
+
var total = document.documentElement.scrollHeight;
|
|
507
|
+
if (!total) return 0;
|
|
508
|
+
var top = el.getBoundingClientRect().top + window.scrollY;
|
|
509
|
+
return Math.max(0, Math.min(100, Math.round((top / total) * 100)));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function contentProps(el, state) {
|
|
513
|
+
return {
|
|
514
|
+
content_name: state.name,
|
|
515
|
+
content_type: state.type,
|
|
516
|
+
segment: state.segment,
|
|
517
|
+
content_instance: state.instanceId,
|
|
518
|
+
content_depth: contentDepth(el),
|
|
519
|
+
exposure_limit: state.exposureLimit
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// The composed event path lets delegated handlers see through open shadow
|
|
524
|
+
// roots, where e.target is retargeted to the host element.
|
|
525
|
+
function eventPath(e) {
|
|
526
|
+
if (e.composedPath) {
|
|
527
|
+
try { return e.composedPath(); } catch (err) { /* fall through */ }
|
|
528
|
+
}
|
|
529
|
+
var path = [];
|
|
530
|
+
var node = e.target;
|
|
531
|
+
while (node) {
|
|
532
|
+
path.push(node);
|
|
533
|
+
node = node.parentNode;
|
|
534
|
+
}
|
|
535
|
+
return path;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function findInPath(path, fromIndex, predicate) {
|
|
539
|
+
for (var i = fromIndex; i < path.length; i++) {
|
|
540
|
+
var node = path[i];
|
|
541
|
+
if (node && node.nodeType === 1 && predicate(node)) return { el: node, index: i };
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function registerContent(el) {
|
|
547
|
+
if (!config.enableContentTracking || contentStates.has(el)) return;
|
|
548
|
+
var name = el.getAttribute('data-vane-content');
|
|
549
|
+
if (!name) return;
|
|
550
|
+
var state = {
|
|
551
|
+
name: name,
|
|
552
|
+
type: el.getAttribute('data-vane-type') || null,
|
|
553
|
+
segment: el.getAttribute('data-vane-segment') || null,
|
|
554
|
+
exposureLimit: parseInt(el.getAttribute('data-vane-exposure'), 10) || config.contentExposureLimit,
|
|
555
|
+
instanceId: uuid(),
|
|
556
|
+
served: true,
|
|
557
|
+
viewed: false,
|
|
558
|
+
clicked: false,
|
|
559
|
+
accumulated: 0,
|
|
560
|
+
visibleSince: null,
|
|
561
|
+
resumeOnShow: false,
|
|
562
|
+
timer: null
|
|
563
|
+
};
|
|
564
|
+
contentStates.set(el, state);
|
|
565
|
+
emit('content_serve', contentProps(el, state));
|
|
566
|
+
if (intersectionObserver) intersectionObserver.observe(el);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function scanContent(root) {
|
|
570
|
+
if (!root) root = document;
|
|
571
|
+
if (root.nodeType === 1) {
|
|
572
|
+
if (config.enableContentTracking && root.hasAttribute('data-vane-content')) {
|
|
573
|
+
registerContent(root);
|
|
574
|
+
}
|
|
575
|
+
if (config.trackShadowDom && root.shadowRoot) attachRoot(root.shadowRoot);
|
|
576
|
+
}
|
|
577
|
+
if (!root.querySelectorAll) return;
|
|
578
|
+
if (config.enableContentTracking) {
|
|
579
|
+
var els = root.querySelectorAll('[data-vane-content]');
|
|
580
|
+
for (var i = 0; i < els.length; i++) registerContent(els[i]);
|
|
581
|
+
}
|
|
582
|
+
if (config.trackShadowDom) {
|
|
583
|
+
var all = root.querySelectorAll('*');
|
|
584
|
+
for (var j = 0; j < all.length; j++) {
|
|
585
|
+
if (all[j].shadowRoot) attachRoot(all[j].shadowRoot);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Each open shadow root needs its own MutationObserver target and its own
|
|
591
|
+
// `submit` listener (submit events are composed: false and never cross the
|
|
592
|
+
// shadow boundary). Clicks and focusin are composed and stay delegated.
|
|
593
|
+
var observedRoots = [];
|
|
594
|
+
|
|
595
|
+
function attachRoot(root) {
|
|
596
|
+
if (observedRoots.indexOf(root) !== -1) return;
|
|
597
|
+
observedRoots.push(root);
|
|
598
|
+
if (mutationObserver) {
|
|
599
|
+
var target = root === document ? (document.body || document.documentElement) : root;
|
|
600
|
+
mutationObserver.observe(target, {
|
|
601
|
+
childList: true,
|
|
602
|
+
subtree: true,
|
|
603
|
+
attributes: true,
|
|
604
|
+
attributeFilter: ['data-vane-content']
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
if (root !== document && config.enableFormTracking) {
|
|
608
|
+
listen(root, 'submit', onSubmit, true);
|
|
609
|
+
}
|
|
610
|
+
scanContent(root);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function patchAttachShadow() {
|
|
614
|
+
if (!window.Element || !Element.prototype.attachShadow) return;
|
|
615
|
+
var original = Element.prototype.attachShadow;
|
|
616
|
+
Element.prototype.attachShadow = function (init) {
|
|
617
|
+
var root = original.call(this, init);
|
|
618
|
+
// Closed roots stay private: their events are retargeted beyond
|
|
619
|
+
// recovery anyway, so only open roots are tracked.
|
|
620
|
+
if (init && init.mode === 'open') attachRoot(root);
|
|
621
|
+
return root;
|
|
622
|
+
};
|
|
623
|
+
cleanups.push(function () { Element.prototype.attachShadow = original; });
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function isContentVisible(entry, viewportHeight) {
|
|
627
|
+
if (!entry.isIntersecting) return false;
|
|
628
|
+
var rect = entry.boundingClientRect;
|
|
629
|
+
// Standard elements must be (essentially) fully visible. Elements taller
|
|
630
|
+
// than the viewport count as visible once they fill enough of it.
|
|
631
|
+
if (rect.height <= viewportHeight) return entry.intersectionRatio >= 0.98;
|
|
632
|
+
return entry.intersectionRect.height >= viewportHeight * config.largeContentViewportFill;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function scheduleViewTimer(el, state) {
|
|
636
|
+
var remaining = Math.max(0, state.exposureLimit - state.accumulated);
|
|
637
|
+
clearTimeout(state.timer);
|
|
638
|
+
state.timer = setTimeout(function () {
|
|
639
|
+
if (state.viewed || state.visibleSince === null) return;
|
|
640
|
+
state.viewed = true;
|
|
641
|
+
var exposure = state.accumulated + (now() - state.visibleSince);
|
|
642
|
+
emit('content_view', assign(contentProps(el, state), {
|
|
643
|
+
exposure_time: Math.round(exposure)
|
|
644
|
+
}));
|
|
645
|
+
if (intersectionObserver) intersectionObserver.unobserve(el);
|
|
646
|
+
}, remaining);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function pauseContent(state) {
|
|
650
|
+
if (state.visibleSince !== null) {
|
|
651
|
+
state.accumulated += now() - state.visibleSince;
|
|
652
|
+
state.visibleSince = null;
|
|
653
|
+
}
|
|
654
|
+
clearTimeout(state.timer);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function onIntersect(entries) {
|
|
658
|
+
var viewportHeight = window.innerHeight;
|
|
659
|
+
for (var i = 0; i < entries.length; i++) {
|
|
660
|
+
var entry = entries[i];
|
|
661
|
+
var state = contentStates.get(entry.target);
|
|
662
|
+
if (!state || state.viewed) continue;
|
|
663
|
+
var visible = isContentVisible(entry, viewportHeight);
|
|
664
|
+
if (visible && state.visibleSince === null) {
|
|
665
|
+
state.visibleSince = now();
|
|
666
|
+
scheduleViewTimer(entry.target, state);
|
|
667
|
+
} else if (!visible && state.visibleSince !== null) {
|
|
668
|
+
pauseContent(state);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function setupDomTracking() {
|
|
674
|
+
if (!config.enableContentTracking && !config.trackShadowDom) return;
|
|
675
|
+
|
|
676
|
+
if (config.enableContentTracking && typeof IntersectionObserver !== 'undefined') {
|
|
677
|
+
intersectionObserver = new IntersectionObserver(onIntersect, {
|
|
678
|
+
threshold: [0, 0.25, 0.5, 0.65, 0.75, 0.9, 0.98, 1]
|
|
679
|
+
});
|
|
680
|
+
cleanups.push(function () { intersectionObserver.disconnect(); intersectionObserver = null; });
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
684
|
+
mutationObserver = new MutationObserver(function (mutations) {
|
|
685
|
+
for (var i = 0; i < mutations.length; i++) {
|
|
686
|
+
var mutation = mutations[i];
|
|
687
|
+
if (mutation.type === 'attributes') {
|
|
688
|
+
registerContent(mutation.target);
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
for (var j = 0; j < mutation.addedNodes.length; j++) {
|
|
692
|
+
var node = mutation.addedNodes[j];
|
|
693
|
+
if (node.nodeType === 1) scanContent(node);
|
|
694
|
+
}
|
|
695
|
+
// Declarative shadow DOM streamed in after load: when the parser
|
|
696
|
+
// finishes a <template shadowrootmode>, it attaches the root and
|
|
697
|
+
// removes the template — this removal is the only observable trace.
|
|
698
|
+
if (config.trackShadowDom) {
|
|
699
|
+
for (var k = 0; k < mutation.removedNodes.length; k++) {
|
|
700
|
+
var removed = mutation.removedNodes[k];
|
|
701
|
+
if (removed.nodeType === 1 && removed.tagName === 'TEMPLATE' &&
|
|
702
|
+
(removed.hasAttribute('shadowrootmode') || removed.hasAttribute('shadowroot')) &&
|
|
703
|
+
mutation.target && mutation.target.nodeType === 1 && mutation.target.shadowRoot) {
|
|
704
|
+
attachRoot(mutation.target.shadowRoot);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
cleanups.push(function () { mutationObserver.disconnect(); mutationObserver = null; });
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (config.trackShadowDom) patchAttachShadow();
|
|
714
|
+
attachRoot(document);
|
|
715
|
+
|
|
716
|
+
// Tab switches don't change intersection state, so pause/resume manually
|
|
717
|
+
listen(document, 'visibilitychange', function () {
|
|
718
|
+
if (document.visibilityState === 'hidden') {
|
|
719
|
+
contentStates.forEach(function (state) {
|
|
720
|
+
if (state.visibleSince !== null) {
|
|
721
|
+
pauseContent(state);
|
|
722
|
+
state.resumeOnShow = true;
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
} else {
|
|
726
|
+
contentStates.forEach(function (state, el) {
|
|
727
|
+
if (state.resumeOnShow && !state.viewed) {
|
|
728
|
+
state.resumeOnShow = false;
|
|
729
|
+
state.visibleSince = now();
|
|
730
|
+
scheduleViewTimer(el, state);
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// ---------------------------------------------------------------------
|
|
738
|
+
// Click tracking (content clicks + links)
|
|
739
|
+
// ---------------------------------------------------------------------
|
|
740
|
+
|
|
741
|
+
function linkType(href) {
|
|
742
|
+
if (/^mailto:/i.test(href)) return 'email';
|
|
743
|
+
if (/^tel:/i.test(href)) return 'phone';
|
|
744
|
+
return 'web';
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function onClick(e) {
|
|
748
|
+
var path = eventPath(e);
|
|
749
|
+
if (!path.length) return;
|
|
750
|
+
touchSession();
|
|
751
|
+
|
|
752
|
+
if (config.enableContentTracking) {
|
|
753
|
+
var clickHit = findInPath(path, 0, function (el) {
|
|
754
|
+
return el.hasAttribute('data-vane-content-click');
|
|
755
|
+
});
|
|
756
|
+
if (clickHit) {
|
|
757
|
+
var clickEl = clickHit.el;
|
|
758
|
+
var containerHit = findInPath(path, clickHit.index, function (el) {
|
|
759
|
+
return el.hasAttribute('data-vane-content');
|
|
760
|
+
});
|
|
761
|
+
var container = containerHit ? containerHit.el : null;
|
|
762
|
+
var state = container ? contentStates.get(container) : null;
|
|
763
|
+
if (state) state.clicked = true;
|
|
764
|
+
var props = {
|
|
765
|
+
click_id: clickEl.getAttribute('data-vane-content-click'),
|
|
766
|
+
element_tag: clickEl.tagName.toLowerCase(),
|
|
767
|
+
element_text: truncate(clickEl.innerText || clickEl.textContent, 100) || null
|
|
768
|
+
};
|
|
769
|
+
if (container && state) assign(props, contentProps(container, state));
|
|
770
|
+
emit('content_click', props);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (config.enableLinkTracking) {
|
|
775
|
+
var anchorHit = findInPath(path, 0, function (el) {
|
|
776
|
+
return el.tagName === 'A' && el.getAttribute('href');
|
|
777
|
+
});
|
|
778
|
+
if (anchorHit) {
|
|
779
|
+
var anchor = anchorHit.el;
|
|
780
|
+
var rawHref = anchor.getAttribute('href') || '';
|
|
781
|
+
emit('link_click', {
|
|
782
|
+
url: anchor.href,
|
|
783
|
+
text: truncate(anchor.innerText || anchor.getAttribute('aria-label'), 100) || null,
|
|
784
|
+
target: anchor.getAttribute('target') || null,
|
|
785
|
+
href: truncate(rawHref.split(/[?#]/)[0], 200),
|
|
786
|
+
link_type: linkType(rawHref),
|
|
787
|
+
is_external: !!anchor.host && anchor.host !== window.location.host
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// ---------------------------------------------------------------------
|
|
794
|
+
// Form tracking (submit + abandonment)
|
|
795
|
+
// ---------------------------------------------------------------------
|
|
796
|
+
|
|
797
|
+
var formEngagement = new Map();
|
|
798
|
+
|
|
799
|
+
function formProps(form) {
|
|
800
|
+
var fields = form.querySelectorAll('input, select, textarea');
|
|
801
|
+
var fieldTypes = [];
|
|
802
|
+
var required = 0;
|
|
803
|
+
for (var i = 0; i < fields.length; i++) {
|
|
804
|
+
var type = fields[i].type || fields[i].tagName.toLowerCase();
|
|
805
|
+
if (fieldTypes.indexOf(type) === -1) fieldTypes.push(type);
|
|
806
|
+
if (fields[i].required) required++;
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
form_name: form.getAttribute('name') || form.id || 'unnamed',
|
|
810
|
+
form_action: form.getAttribute('action') || window.location.pathname,
|
|
811
|
+
form_method: (form.getAttribute('method') || 'GET').toUpperCase(),
|
|
812
|
+
field_count: fields.length,
|
|
813
|
+
field_types: fieldTypes,
|
|
814
|
+
required_fields: required,
|
|
815
|
+
optional_fields: fields.length - required,
|
|
816
|
+
form_type: form.getAttribute('data-vane-form-type') || null,
|
|
817
|
+
form_category: form.getAttribute('data-vane-form-category') || null,
|
|
818
|
+
form_step: form.getAttribute('data-vane-form-step') || null,
|
|
819
|
+
form_funnel: form.getAttribute('data-vane-form-funnel') || null,
|
|
820
|
+
form_value: form.getAttribute('data-vane-form-value') || null,
|
|
821
|
+
form_goal: form.getAttribute('data-vane-form-goal') || null,
|
|
822
|
+
form_segment: form.getAttribute('data-vane-form-segment') || null
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function onFocusIn(e) {
|
|
827
|
+
var field = eventPath(e)[0];
|
|
828
|
+
if (!field || field.nodeType !== 1 || !field.form) return;
|
|
829
|
+
if (!formEngagement.has(field.form)) {
|
|
830
|
+
formEngagement.set(field.form, { start: now() });
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function onSubmit(e) {
|
|
835
|
+
var form = e.target;
|
|
836
|
+
if (!form || form.tagName !== 'FORM') return;
|
|
837
|
+
var props = formProps(form);
|
|
838
|
+
var engagement = formEngagement.get(form);
|
|
839
|
+
if (engagement) {
|
|
840
|
+
props.completion_time = now() - engagement.start;
|
|
841
|
+
formEngagement.delete(form);
|
|
842
|
+
}
|
|
843
|
+
emit('form_submit', props);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function flushFormAbandons() {
|
|
847
|
+
if (!config.enableFormTracking) return;
|
|
848
|
+
formEngagement.forEach(function (engagement, form) {
|
|
849
|
+
var engagementTime = now() - engagement.start;
|
|
850
|
+
if (engagementTime >= config.formAbandonThreshold) {
|
|
851
|
+
emit('form_abandon', assign(formProps(form), { engagement_time: engagementTime }));
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
formEngagement.clear();
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// ---------------------------------------------------------------------
|
|
858
|
+
// Public API
|
|
859
|
+
// ---------------------------------------------------------------------
|
|
860
|
+
|
|
861
|
+
function init(options) {
|
|
862
|
+
if (ready) {
|
|
863
|
+
log('init() called twice — ignoring');
|
|
864
|
+
return api;
|
|
865
|
+
}
|
|
866
|
+
assign(config, options || {});
|
|
867
|
+
ready = true;
|
|
868
|
+
|
|
869
|
+
loadClientId();
|
|
870
|
+
currentUtm = parseUtm();
|
|
871
|
+
observeVitals();
|
|
872
|
+
observeScroll();
|
|
873
|
+
loadSession(); // may emit session_start
|
|
874
|
+
|
|
875
|
+
if (config.enableDynamicPageview) patchHistory();
|
|
876
|
+
listen(document, 'click', onClick, true);
|
|
877
|
+
if (config.enableFormTracking) {
|
|
878
|
+
listen(document, 'focusin', onFocusIn, true);
|
|
879
|
+
listen(document, 'submit', onSubmit, true);
|
|
880
|
+
listen(window, 'pagehide', flushFormAbandons);
|
|
881
|
+
}
|
|
882
|
+
listen(document, 'keydown', touchSession, { passive: true });
|
|
883
|
+
listen(window, 'scroll', touchSession, { passive: true });
|
|
884
|
+
listen(window, 'pagehide', persistSession);
|
|
885
|
+
|
|
886
|
+
whenDomReady(function () {
|
|
887
|
+
if (config.enableAutoPageview) {
|
|
888
|
+
startPageView('pageview', assign({ navigation_trigger: 'initial' }, navTiming(), connectionInfo()));
|
|
889
|
+
} else {
|
|
890
|
+
pageViewId = uuid();
|
|
891
|
+
}
|
|
892
|
+
setupDomTracking();
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
// Replay anything tracked before init
|
|
896
|
+
var queued = pendingEvents.splice(0);
|
|
897
|
+
for (var i = 0; i < queued.length; i++) emit(queued[i][0], queued[i][1]);
|
|
898
|
+
|
|
899
|
+
log('initialized', config);
|
|
900
|
+
return api;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function track(eventName, properties) {
|
|
904
|
+
if (!eventName || typeof eventName !== 'string') {
|
|
905
|
+
log('track() requires an event name string');
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
return emit(eventName, properties);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function trackPageView() {
|
|
912
|
+
startPageView('pageview', { navigation_trigger: 'manual' });
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function on(eventName, callback, options) {
|
|
916
|
+
options = options || {};
|
|
917
|
+
var type = config.eventPrefix + ':' + (eventName === '*' ? 'event' : eventName);
|
|
918
|
+
var handler = function (e) { callback(e.detail, e); };
|
|
919
|
+
window.addEventListener(type, handler);
|
|
920
|
+
if (options.replay) {
|
|
921
|
+
for (var i = 0; i < eventHistory.length; i++) {
|
|
922
|
+
if (eventName === '*' || eventHistory[i].event_name === eventName) {
|
|
923
|
+
callback(eventHistory[i], null);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return function off() { window.removeEventListener(type, handler); };
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function setUserId(id) {
|
|
931
|
+
var previous = userId;
|
|
932
|
+
userId = id || null;
|
|
933
|
+
if (userId && userId !== previous) {
|
|
934
|
+
emit('user_identify', { user_id: userId, previous_user_id: previous });
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function setContext(key, value) {
|
|
939
|
+
if (value === undefined || value === null) {
|
|
940
|
+
delete context[key];
|
|
941
|
+
} else {
|
|
942
|
+
context[key] = value;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function destroy() {
|
|
947
|
+
flushFormAbandons();
|
|
948
|
+
contentStates.forEach(function (state) { clearTimeout(state.timer); });
|
|
949
|
+
contentStates.clear();
|
|
950
|
+
formEngagement.clear();
|
|
951
|
+
vitalsObservers.forEach(function (observer) {
|
|
952
|
+
try { observer.disconnect(); } catch (e) { /* already disconnected */ }
|
|
953
|
+
});
|
|
954
|
+
vitalsObservers = [];
|
|
955
|
+
cleanups.forEach(function (fn) {
|
|
956
|
+
try { fn(); } catch (e) { /* best effort */ }
|
|
957
|
+
});
|
|
958
|
+
cleanups = [];
|
|
959
|
+
observedRoots = [];
|
|
960
|
+
historyPatched = false;
|
|
961
|
+
ready = false;
|
|
962
|
+
log('destroyed');
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
var api = {
|
|
966
|
+
__loaded: true,
|
|
967
|
+
version: VERSION,
|
|
968
|
+
init: init,
|
|
969
|
+
track: track,
|
|
970
|
+
trackPageView: trackPageView,
|
|
971
|
+
on: on,
|
|
972
|
+
setUserId: setUserId,
|
|
973
|
+
getUserId: function () { return userId; },
|
|
974
|
+
setContext: setContext,
|
|
975
|
+
getContext: function () { return assign({}, context); },
|
|
976
|
+
clearContext: function () { context = {}; },
|
|
977
|
+
newSession: function () { startNewSession('manual'); },
|
|
978
|
+
getClientId: function () { return clientId; },
|
|
979
|
+
getSessionId: function () { return session.id; },
|
|
980
|
+
getPageViewId: function () { return pageViewId; },
|
|
981
|
+
getHistory: function () { return eventHistory.slice(); },
|
|
982
|
+
getContentState: function () {
|
|
983
|
+
var out = [];
|
|
984
|
+
contentStates.forEach(function (state) {
|
|
985
|
+
out.push({
|
|
986
|
+
content_name: state.name,
|
|
987
|
+
content_type: state.type,
|
|
988
|
+
segment: state.segment,
|
|
989
|
+
served: state.served,
|
|
990
|
+
viewed: state.viewed,
|
|
991
|
+
clicked: state.clicked,
|
|
992
|
+
exposure_limit: state.exposureLimit
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
return out;
|
|
996
|
+
},
|
|
997
|
+
isReady: function () { return ready; },
|
|
998
|
+
destroy: destroy
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
// Replace any loader stub and replay its queued calls
|
|
1002
|
+
var queuedCalls = existing && existing._queue;
|
|
1003
|
+
window.vane = api;
|
|
1004
|
+
if (queuedCalls && queuedCalls.length) {
|
|
1005
|
+
for (var q = 0; q < queuedCalls.length; q++) {
|
|
1006
|
+
var call = queuedCalls[q];
|
|
1007
|
+
if (typeof api[call[0]] === 'function') api[call[0]].apply(api, call[1]);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Auto-init unless explicitly disabled via
|
|
1012
|
+
// <script src="weathervane.js" data-vane-auto="false">
|
|
1013
|
+
// or `window.vaneConfig = { autoInit: false }` before this script loads.
|
|
1014
|
+
var currentScript = document.currentScript;
|
|
1015
|
+
var autoDisabled =
|
|
1016
|
+
(currentScript && currentScript.getAttribute('data-vane-auto') === 'false') ||
|
|
1017
|
+
(window.vaneConfig && window.vaneConfig.autoInit === false);
|
|
1018
|
+
if (!autoDisabled && !ready) {
|
|
1019
|
+
init(window.vaneConfig || {});
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
1023
|
+
module.exports = api;
|
|
1024
|
+
}
|
|
1025
|
+
})(typeof window !== 'undefined' ? window : null,
|
|
1026
|
+
typeof document !== 'undefined' ? document : null);
|