@manototh/do11y 0.0.1

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/do11y.js ADDED
@@ -0,0 +1,1099 @@
1
+ (function() {
2
+ //#region src/do11y.ts
3
+ const VERSION = "0.0.1";
4
+ const _alreadyLoaded = !!window.__do11yInitialized;
5
+ window.__do11yInitialized = true;
6
+ const config = {
7
+ destination: "supabase",
8
+ supabaseUrl: "",
9
+ supabaseKey: "",
10
+ supabaseTable: "do11y_events",
11
+ httpEndpoint: "",
12
+ httpHeaders: {},
13
+ debug: false,
14
+ flushInterval: 5e3,
15
+ maxBatchSize: 10,
16
+ trackOutboundLinks: true,
17
+ trackInternalLinks: true,
18
+ trackScrollDepth: true,
19
+ scrollThresholds: [
20
+ 25,
21
+ 50,
22
+ 75,
23
+ 90
24
+ ],
25
+ allowedDomains: null,
26
+ respectDNT: true,
27
+ maxRetries: 2,
28
+ retryDelay: 1e3,
29
+ rateLimitMs: 100,
30
+ framework: "mintlify",
31
+ trackSectionVisibility: true,
32
+ sectionVisibleThreshold: 3,
33
+ trackTabSwitches: true,
34
+ trackTocClicks: true,
35
+ trackExpandCollapse: true,
36
+ trackFeedback: true,
37
+ tabContainerSelector: null,
38
+ tocSelector: null,
39
+ feedbackSelector: null,
40
+ searchSelector: null,
41
+ copyButtonSelector: null,
42
+ codeBlockSelector: null,
43
+ navigationSelector: null,
44
+ footerSelector: null,
45
+ contentSelector: null
46
+ };
47
+ const FRAMEWORK_PRESETS = {
48
+ mintlify: {
49
+ searchSelector: "#search-bar-entry, #search-bar-entry-mobile, [class*=\"search\"]",
50
+ copyButtonSelector: "[class*=\"copy\"], button[aria-label*=\"copy\" i]",
51
+ codeBlockSelector: "pre, [class*=\"code\"]",
52
+ navigationSelector: "nav, [role=\"navigation\"], #navbar, #sidebar, [class*=\"nav\"], [class*=\"sidebar\"]",
53
+ footerSelector: "footer, [role=\"contentinfo\"], [class*=\"footer\"]",
54
+ contentSelector: "main, article, [role=\"main\"], [class*=\"content\"]",
55
+ tabContainerSelector: "[role=\"tablist\"], [class*=\"tab\"]",
56
+ tocSelector: "#table-of-contents, [data-testid=\"table-of-contents\"], [class*=\"table-of-contents\"], [class*=\"toc\"]",
57
+ feedbackSelector: "[class*=\"feedback\"], [class*=\"helpful\"]"
58
+ },
59
+ docusaurus: {
60
+ searchSelector: ".DocSearch, .DocSearch-Button",
61
+ copyButtonSelector: "button.clean-btn[aria-label*=\"copy\" i], button[class*=\"copyButton\"]",
62
+ codeBlockSelector: "pre, [class*=\"code\"]",
63
+ navigationSelector: "nav, [role=\"navigation\"], .navbar, .sidebar, [class*=\"nav\"], [class*=\"sidebar\"]",
64
+ footerSelector: "footer, [role=\"contentinfo\"], [class*=\"footer\"]",
65
+ contentSelector: "main, article, [role=\"main\"], [class*=\"content\"]",
66
+ tabContainerSelector: ".tabs[role=\"tablist\"], [class*=\"tabs\"]",
67
+ tocSelector: ".table-of-contents, [class*=\"toc\"]",
68
+ feedbackSelector: "[class*=\"feedback\"], [class*=\"helpful\"]"
69
+ },
70
+ nextra: {
71
+ searchSelector: ".nextra-search input, input[placeholder*=\"search\" i], button[aria-label*=\"search\" i]",
72
+ copyButtonSelector: "button[class*=\"copy\"], button[aria-label*=\"copy\" i], button[title*=\"copy\" i]",
73
+ codeBlockSelector: "pre, [class*=\"code\"]",
74
+ navigationSelector: "nav, [role=\"navigation\"], [class*=\"nav\"], [class*=\"sidebar\"]",
75
+ footerSelector: "footer, [role=\"contentinfo\"], [class*=\"footer\"]",
76
+ contentSelector: "main, article, [role=\"main\"], [class*=\"content\"]",
77
+ tabContainerSelector: "[role=\"tablist\"], [class*=\"tab\"]",
78
+ tocSelector: ".nextra-toc, [class*=\"toc\"]",
79
+ feedbackSelector: "[class*=\"feedback\"], [class*=\"helpful\"]"
80
+ },
81
+ gitbook: {
82
+ searchSelector: "[data-testid*=\"search\"], button[aria-label*=\"search\" i]",
83
+ copyButtonSelector: "[class*=\"copy\"], button[aria-label*=\"copy\" i]",
84
+ codeBlockSelector: "pre, code, [class*=\"code\"]",
85
+ navigationSelector: "nav, [role=\"navigation\"], [class*=\"nav\"], [class*=\"sidebar\"]",
86
+ footerSelector: "footer, [role=\"contentinfo\"], [class*=\"footer\"]",
87
+ contentSelector: "main, article, [role=\"main\"], [class*=\"content\"]",
88
+ tabContainerSelector: "[role=\"tablist\"], [class*=\"tab\"]",
89
+ tocSelector: "[class*=\"table-of-contents\"], [class*=\"toc\"], [class*=\"page-outline\"]",
90
+ feedbackSelector: "[class*=\"feedback\"], [class*=\"helpful\"], [class*=\"rating\"]"
91
+ },
92
+ "mkdocs-material": {
93
+ searchSelector: ".md-search__input",
94
+ copyButtonSelector: ".md-clipboard, .md-code__button[title=\"Copy to clipboard\"]",
95
+ codeBlockSelector: "pre, code, [class*=\"code\"]",
96
+ navigationSelector: "nav, [role=\"navigation\"], .md-nav, .md-sidebar",
97
+ footerSelector: "footer, [role=\"contentinfo\"], .md-footer",
98
+ contentSelector: "main, article, [role=\"main\"], .md-content",
99
+ tabContainerSelector: ".tabbed-labels, .md-typeset .tabbed-set",
100
+ tocSelector: ".md-sidebar--secondary .md-nav, [class*=\"toc\"]",
101
+ feedbackSelector: "[class*=\"feedback\"], [class*=\"helpful\"]"
102
+ },
103
+ vitepress: {
104
+ searchSelector: ".VPNavBarSearch button, .VPNavBarSearchButton, #local-search",
105
+ copyButtonSelector: ".vp-code-copy, button.copy[title*=\"Copy\"]",
106
+ codeBlockSelector: "pre, [class*=\"code\"]",
107
+ navigationSelector: "nav, [role=\"navigation\"], .VPNav, .VPSidebar, [class*=\"nav\"], [class*=\"sidebar\"]",
108
+ footerSelector: "footer, [role=\"contentinfo\"], .VPFooter, [class*=\"footer\"]",
109
+ contentSelector: "main, article, [role=\"main\"], .VPContent, [class*=\"content\"]",
110
+ tabContainerSelector: ".vp-code-group .tabs, [role=\"tablist\"]",
111
+ tocSelector: ".VPDocAsideOutline, [class*=\"toc\"]",
112
+ feedbackSelector: "[class*=\"feedback\"], [class*=\"helpful\"]"
113
+ }
114
+ };
115
+ const SELECTOR_KEYS = [
116
+ "searchSelector",
117
+ "copyButtonSelector",
118
+ "codeBlockSelector",
119
+ "navigationSelector",
120
+ "footerSelector",
121
+ "contentSelector",
122
+ "tabContainerSelector",
123
+ "tocSelector",
124
+ "feedbackSelector"
125
+ ];
126
+ /**
127
+ * Apply framework-specific selectors to the config.
128
+ * For 'custom', uses whatever the user set in config; for named
129
+ * frameworks, loads the preset and lets explicit config values override.
130
+ */
131
+ function applyFrameworkSelectors() {
132
+ const preset = FRAMEWORK_PRESETS[config.framework];
133
+ if (preset) SELECTOR_KEYS.forEach((key) => {
134
+ if (!config[key]) config[key] = preset[key];
135
+ });
136
+ else if (config.framework !== "custom") {
137
+ if (config.debug) console.warn(`[Do11y] Unknown framework "${config.framework}". Falling back to generic selectors. Supported: ` + Object.keys(FRAMEWORK_PRESETS).join(", ") + ", custom");
138
+ }
139
+ const fallback = FRAMEWORK_PRESETS.mintlify;
140
+ if (!fallback) return;
141
+ SELECTOR_KEYS.forEach((key) => {
142
+ if (!config[key]) config[key] = fallback[key];
143
+ });
144
+ }
145
+ function shouldDisableTracking() {
146
+ if (config.respectDNT && (navigator.doNotTrack === "1" || navigator.doNotTrack === "yes" || window.doNotTrack === "1")) {
147
+ if (config.debug) console.log("[Do11y] Disabled: Do Not Track is enabled");
148
+ return true;
149
+ }
150
+ if (config.allowedDomains && config.allowedDomains.length > 0) {
151
+ const currentDomain = window.location.hostname;
152
+ if (!config.allowedDomains.some((domain) => {
153
+ return currentDomain === domain || currentDomain.endsWith("." + domain);
154
+ })) {
155
+ if (config.debug) console.log("[Do11y] Disabled: Domain not allowed:", currentDomain);
156
+ return true;
157
+ }
158
+ }
159
+ return false;
160
+ }
161
+ /**
162
+ * Validate a CSS selector string supplied through user configuration.
163
+ * Returns the selector unchanged if it is syntactically valid, or null
164
+ * if it is not. This prevents CSS selector injection from attacker-
165
+ * controlled config values (window.Do11yConfig / meta tags) reaching
166
+ * querySelectorAll / closest calls.
167
+ */
168
+ function validateSelector(selector) {
169
+ if (!selector || typeof selector !== "string") return null;
170
+ try {
171
+ document.querySelector(selector);
172
+ return selector;
173
+ } catch {
174
+ if (config.debug) console.warn("[Do11y] Invalid CSS selector rejected:", selector);
175
+ return null;
176
+ }
177
+ }
178
+ function sanitizeText(text, maxLength) {
179
+ if (!text || typeof text !== "string") return null;
180
+ const limit = maxLength ?? 100;
181
+ let sanitized = text;
182
+ sanitized = sanitized.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, "[email]");
183
+ sanitized = sanitized.replace(/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, "[phone]");
184
+ sanitized = sanitized.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[redacted]");
185
+ sanitized = sanitized.replace(/\b(?:\d[ -]?){13,19}\b/g, "[card]");
186
+ sanitized = sanitized.replace(/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "[token]");
187
+ sanitized = sanitized.replace(/\bxa[a-z]{2}-[A-Za-z0-9_-]{20,}/g, "[token]");
188
+ sanitized = sanitized.replace(/\b[0-9a-fA-F]{32,}\b/g, "[redacted]");
189
+ return sanitized.trim().substring(0, limit);
190
+ }
191
+ function generateSessionId() {
192
+ if (window.crypto && typeof window.crypto.randomUUID === "function") return window.crypto.randomUUID();
193
+ if (window.crypto && typeof window.crypto.getRandomValues === "function") {
194
+ const arr = new Uint8Array(16);
195
+ window.crypto.getRandomValues(arr);
196
+ arr[6] = arr[6] & 15 | 64;
197
+ arr[8] = arr[8] & 63 | 128;
198
+ const hex = Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
199
+ return hex.slice(0, 8) + "-" + hex.slice(8, 12) + "-" + hex.slice(12, 16) + "-" + hex.slice(16, 20) + "-" + hex.slice(20);
200
+ }
201
+ return "no-crypto-00-0000-0000-000000000000";
202
+ }
203
+ function isValidSessionData(value) {
204
+ if (!value || typeof value !== "object") return false;
205
+ const v = value;
206
+ return typeof v.id === "string" && v.id.length > 0 && typeof v.startTime === "string" && Array.isArray(v.pageSequence) && typeof v.pageCount === "number";
207
+ }
208
+ function getSession() {
209
+ let session = null;
210
+ try {
211
+ const stored = sessionStorage.getItem("do11y_session");
212
+ if (stored) {
213
+ const parsed = JSON.parse(stored);
214
+ if (isValidSessionData(parsed)) session = parsed;
215
+ }
216
+ } catch {}
217
+ if (!session) {
218
+ session = {
219
+ id: generateSessionId(),
220
+ startTime: (/* @__PURE__ */ new Date()).toISOString(),
221
+ pageSequence: [],
222
+ pageCount: 0,
223
+ referrerCategory: null,
224
+ aiPlatform: null
225
+ };
226
+ saveSession(session);
227
+ }
228
+ return session;
229
+ }
230
+ function saveSession(session) {
231
+ try {
232
+ sessionStorage.setItem("do11y_session", JSON.stringify(session));
233
+ } catch {}
234
+ }
235
+ function updatePageSequence(path) {
236
+ const session = getSession();
237
+ session.pageCount++;
238
+ session.pageSequence.push({
239
+ path,
240
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
241
+ index: session.pageCount
242
+ });
243
+ if (session.pageSequence.length > 50) session.pageSequence = session.pageSequence.slice(-50);
244
+ saveSession(session);
245
+ return session;
246
+ }
247
+ function getBrowserContext() {
248
+ return {
249
+ viewportCategory: categorizeViewport(),
250
+ browserFamily: getBrowserFamily(),
251
+ deviceType: getDeviceType(),
252
+ language: (navigator.language || "").split("-")[0] || "unknown",
253
+ timezoneOffset: (/* @__PURE__ */ new Date()).getTimezoneOffset() / 60
254
+ };
255
+ }
256
+ function categorizeViewport() {
257
+ const width = window.innerWidth;
258
+ if (width < 640) return "mobile";
259
+ if (width < 1024) return "tablet";
260
+ if (width < 1440) return "desktop";
261
+ return "large-desktop";
262
+ }
263
+ function getBrowserFamily() {
264
+ const ua = navigator.userAgent;
265
+ if (ua.includes("Firefox")) return "Firefox";
266
+ if (ua.includes("Edg")) return "Edge";
267
+ if (ua.includes("Chrome")) return "Chrome";
268
+ if (ua.includes("Safari")) return "Safari";
269
+ return "Other";
270
+ }
271
+ function getDeviceType() {
272
+ const ua = navigator.userAgent;
273
+ if (/Mobile|Android|iPhone|iPad/.test(ua)) {
274
+ if (/iPad|Tablet/.test(ua)) return "tablet";
275
+ return "mobile";
276
+ }
277
+ return "desktop";
278
+ }
279
+ /**
280
+ * Known AI platform referrer patterns.
281
+ * Each entry maps a substring found in the referrer hostname to an AI
282
+ * platform label. Order matters: first match wins.
283
+ */
284
+ const AI_REFERRER_PATTERNS = [
285
+ {
286
+ match: "chatgpt",
287
+ platform: "ChatGPT"
288
+ },
289
+ {
290
+ match: "chat.com",
291
+ platform: "ChatGPT"
292
+ },
293
+ {
294
+ match: "openai",
295
+ platform: "ChatGPT"
296
+ },
297
+ {
298
+ match: "perplexity",
299
+ platform: "Perplexity"
300
+ },
301
+ {
302
+ match: "claude.ai",
303
+ platform: "Claude"
304
+ },
305
+ {
306
+ match: "anthropic",
307
+ platform: "Claude"
308
+ },
309
+ {
310
+ match: "gemini",
311
+ platform: "Gemini"
312
+ },
313
+ {
314
+ match: "copilot",
315
+ platform: "Copilot"
316
+ },
317
+ {
318
+ match: "deepseek",
319
+ platform: "DeepSeek"
320
+ },
321
+ {
322
+ match: "meta.ai",
323
+ platform: "Meta AI"
324
+ },
325
+ {
326
+ match: "grok",
327
+ platform: "Grok"
328
+ },
329
+ {
330
+ match: "x.ai",
331
+ platform: "Grok"
332
+ },
333
+ {
334
+ match: "mistral",
335
+ platform: "Mistral"
336
+ },
337
+ {
338
+ match: "you.com",
339
+ platform: "You.com"
340
+ },
341
+ {
342
+ match: "phind",
343
+ platform: "Phind"
344
+ }
345
+ ];
346
+ /**
347
+ * Classify a referrer hostname into a traffic source category.
348
+ * Returns { referrerCategory, aiPlatform } where aiPlatform is null
349
+ * for non-AI traffic.
350
+ */
351
+ function classifyReferrer(hostname) {
352
+ if (!hostname || hostname === "direct") return {
353
+ referrerCategory: "direct",
354
+ aiPlatform: null
355
+ };
356
+ if (hostname === "internal") return {
357
+ referrerCategory: "internal",
358
+ aiPlatform: null
359
+ };
360
+ if (hostname === "unknown") return {
361
+ referrerCategory: "unknown",
362
+ aiPlatform: null
363
+ };
364
+ const h = hostname.toLowerCase();
365
+ for (const pattern of AI_REFERRER_PATTERNS) if (h.indexOf(pattern.match) !== -1) return {
366
+ referrerCategory: "ai",
367
+ aiPlatform: pattern.platform
368
+ };
369
+ if (/google\.|bing\.|baidu\.|yandex\.|duckduckgo\.|yahoo\./.test(h)) return {
370
+ referrerCategory: "search-engine",
371
+ aiPlatform: null
372
+ };
373
+ if (/github\.|gitlab\.|bitbucket\./.test(h)) return {
374
+ referrerCategory: "code-host",
375
+ aiPlatform: null
376
+ };
377
+ if (/stackoverflow\.|stackexchange\.|reddit\.|news\.ycombinator\./.test(h)) return {
378
+ referrerCategory: "community",
379
+ aiPlatform: null
380
+ };
381
+ if (/twitter\.|x\.com|linkedin\.|facebook\.|threads\.net/.test(h)) return {
382
+ referrerCategory: "social",
383
+ aiPlatform: null
384
+ };
385
+ return {
386
+ referrerCategory: "other",
387
+ aiPlatform: null
388
+ };
389
+ }
390
+ function getReferrerDomain() {
391
+ try {
392
+ if (!document.referrer) return "direct";
393
+ const url = new URL(document.referrer);
394
+ if (url.hostname === window.location.hostname) return "internal";
395
+ return url.hostname;
396
+ } catch {
397
+ return "unknown";
398
+ }
399
+ }
400
+ function getPageInfo() {
401
+ return {
402
+ path: window.location.pathname,
403
+ hash: window.location.hash || null,
404
+ search: window.location.search ? "has_params" : null,
405
+ title: sanitizeText(document.title, 150)
406
+ };
407
+ }
408
+ let eventQueue = [];
409
+ let flushTimeout = null;
410
+ const lastEventTime = {};
411
+ let isDisabled = false;
412
+ function queueEvent(eventType, eventData) {
413
+ if (isDisabled) return;
414
+ const now = Date.now();
415
+ if (config.rateLimitMs > 0 && lastEventTime[eventType]) {
416
+ if (now - lastEventTime[eventType] < config.rateLimitMs) {
417
+ if (config.debug) console.log("[Do11y] Rate limited:", eventType);
418
+ return;
419
+ }
420
+ }
421
+ lastEventTime[eventType] = now;
422
+ const session = getSession();
423
+ const event = {
424
+ _time: (/* @__PURE__ */ new Date()).toISOString(),
425
+ eventType,
426
+ "do11y_version": VERSION,
427
+ sessionId: session.id,
428
+ sessionPageCount: session.pageCount,
429
+ ...getPageInfo(),
430
+ ...getBrowserContext(),
431
+ ...eventData
432
+ };
433
+ if (config.debug) console.log("[Do11y] Event queued:", event);
434
+ eventQueue.push(event);
435
+ if (eventQueue.length > 100) {
436
+ eventQueue = eventQueue.slice(-100);
437
+ if (config.debug) console.warn("[Do11y] Event queue capped at 100 events");
438
+ }
439
+ if (eventQueue.length >= config.maxBatchSize) flush();
440
+ else scheduleFlush();
441
+ }
442
+ function scheduleFlush() {
443
+ if (flushTimeout) return;
444
+ flushTimeout = setTimeout(flush, config.flushInterval);
445
+ }
446
+ function validateSupabaseUrl(url) {
447
+ try {
448
+ const parsed = new URL(url);
449
+ if (parsed.protocol !== "https:") return false;
450
+ if (!parsed.hostname.endsWith(".supabase.co")) return false;
451
+ return true;
452
+ } catch {
453
+ return false;
454
+ }
455
+ }
456
+ function validateHttpEndpoint(url) {
457
+ try {
458
+ const parsed = new URL(url);
459
+ if (parsed.protocol !== "https:") return false;
460
+ const host = parsed.hostname;
461
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1") return false;
462
+ if (/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/.test(host)) return false;
463
+ return true;
464
+ } catch {
465
+ return false;
466
+ }
467
+ }
468
+ function validateConfig() {
469
+ if (config.destination === "supabase") {
470
+ if (!config.supabaseUrl) {
471
+ if (config.debug) console.warn("[Do11y] No Supabase URL configured");
472
+ return false;
473
+ }
474
+ if (!validateSupabaseUrl(config.supabaseUrl)) {
475
+ if (config.debug) console.warn("[Do11y] Invalid Supabase URL. Must be https://<project>.supabase.co");
476
+ return false;
477
+ }
478
+ if (!config.supabaseKey || typeof config.supabaseKey !== "string" || config.supabaseKey.length < 10) {
479
+ if (config.debug) console.warn("[Do11y] Invalid or missing Supabase publishable key");
480
+ return false;
481
+ }
482
+ if (!/^[a-zA-Z0-9_-]+$/.test(config.supabaseTable)) {
483
+ if (config.debug) console.warn("[Do11y] Invalid table name");
484
+ return false;
485
+ }
486
+ return true;
487
+ }
488
+ if (config.destination === "http") {
489
+ if (!config.httpEndpoint) {
490
+ if (config.debug) console.warn("[Do11y] No HTTP endpoint configured");
491
+ return false;
492
+ }
493
+ if (!validateHttpEndpoint(config.httpEndpoint)) {
494
+ if (config.debug) console.warn("[Do11y] Invalid HTTP endpoint. Must be HTTPS and not a private address.");
495
+ return false;
496
+ }
497
+ return true;
498
+ }
499
+ if (config.debug) console.warn("[Do11y] Unknown destination:", config.destination);
500
+ return false;
501
+ }
502
+ function buildRequest(events) {
503
+ if (config.destination === "supabase") return {
504
+ url: config.supabaseUrl.replace(/\/$/, "") + "/rest/v1/" + config.supabaseTable,
505
+ headers: {
506
+ "apikey": config.supabaseKey,
507
+ "Authorization": "Bearer " + config.supabaseKey,
508
+ "Content-Type": "application/json",
509
+ "Prefer": "return=minimal"
510
+ },
511
+ body: JSON.stringify(events.map((e) => ({ payload: e })))
512
+ };
513
+ return {
514
+ url: config.httpEndpoint,
515
+ headers: {
516
+ "Content-Type": "application/json",
517
+ ...config.httpHeaders
518
+ },
519
+ body: JSON.stringify(events)
520
+ };
521
+ }
522
+ function flush(retriesLeft) {
523
+ if (flushTimeout) {
524
+ clearTimeout(flushTimeout);
525
+ flushTimeout = null;
526
+ }
527
+ if (eventQueue.length === 0) return;
528
+ if (!validateConfig()) return;
529
+ const retries = typeof retriesLeft === "number" ? retriesLeft : config.maxRetries;
530
+ const events = eventQueue.slice();
531
+ eventQueue = [];
532
+ sendEvents(buildRequest(events), events, retries);
533
+ }
534
+ function sendEvents(req, events, retriesLeft) {
535
+ fetch(req.url, {
536
+ method: "POST",
537
+ headers: req.headers,
538
+ body: req.body,
539
+ keepalive: true
540
+ }).then((response) => {
541
+ if (response.ok) {
542
+ if (config.debug) console.log("[Do11y] Flushed", events.length, "events");
543
+ return;
544
+ }
545
+ if (retriesLeft > 0 && (response.status >= 500 || response.status === 429)) {
546
+ if (config.debug) console.log("[Do11y] Retrying after error:", response.status);
547
+ eventQueue = events.concat(eventQueue);
548
+ setTimeout(() => {
549
+ flush(retriesLeft - 1);
550
+ }, config.retryDelay * (config.maxRetries - retriesLeft + 1));
551
+ return;
552
+ }
553
+ if (config.debug) response.text().then((text) => {
554
+ console.error("[Do11y] Ingest failed:", response.status, text);
555
+ }).catch(() => {});
556
+ }).catch((err) => {
557
+ if (retriesLeft > 0) {
558
+ if (config.debug) console.log("[Do11y] Network error, retrying:", err.message);
559
+ eventQueue = events.concat(eventQueue);
560
+ setTimeout(() => {
561
+ flush(retriesLeft - 1);
562
+ }, config.retryDelay * (config.maxRetries - retriesLeft + 1));
563
+ } else if (config.debug) console.error("[Do11y] Failed to send events:", err);
564
+ });
565
+ }
566
+ function flushSync() {
567
+ if (eventQueue.length === 0) return;
568
+ if (!validateConfig()) return;
569
+ const events = eventQueue;
570
+ eventQueue = [];
571
+ const req = buildRequest(events);
572
+ try {
573
+ fetch(req.url, {
574
+ method: "POST",
575
+ headers: req.headers,
576
+ body: req.body,
577
+ keepalive: true
578
+ });
579
+ } catch {}
580
+ if (config.debug) console.log("[Do11y] Sync flushed", events.length, "events");
581
+ }
582
+ function trackPageView() {
583
+ const session = updatePageSequence(window.location.pathname);
584
+ const referrerDomain = getReferrerDomain();
585
+ const referrerInfo = classifyReferrer(referrerDomain);
586
+ if (session.pageCount === 1) {
587
+ session.referrerCategory = referrerInfo.referrerCategory;
588
+ session.aiPlatform = referrerInfo.aiPlatform;
589
+ saveSession(session);
590
+ }
591
+ queueEvent("page_view", {
592
+ referrerDomain,
593
+ referrerCategory: referrerInfo.referrerCategory,
594
+ aiPlatform: referrerInfo.aiPlatform,
595
+ isFirstPage: session.pageCount === 1,
596
+ previousPath: session.pageSequence.length > 1 ? session.pageSequence[session.pageSequence.length - 2].path : null
597
+ });
598
+ }
599
+ function setupLinkTracking() {
600
+ document.addEventListener("click", (e) => {
601
+ const link = e.target.closest("a");
602
+ if (!link) return;
603
+ const href = link.getAttribute("href");
604
+ if (!href) return;
605
+ let linkType = "other";
606
+ let targetDomain = null;
607
+ try {
608
+ if (href.startsWith("#")) linkType = "anchor";
609
+ else if (href.startsWith("/") || href.startsWith("./") || href.startsWith("../")) linkType = "internal";
610
+ else if (href.startsWith("http")) {
611
+ const url = new URL(href);
612
+ if (url.hostname === window.location.hostname) linkType = "internal";
613
+ else {
614
+ linkType = "external";
615
+ targetDomain = url.hostname;
616
+ }
617
+ } else if (href.startsWith("mailto:")) linkType = "email";
618
+ } catch {}
619
+ if (linkType === "internal" && !config.trackInternalLinks) return;
620
+ if (linkType === "external" && !config.trackOutboundLinks) return;
621
+ queueEvent("link_click", {
622
+ linkType,
623
+ targetUrl: href,
624
+ targetDomain,
625
+ linkText: sanitizeText(link.textContent, 100),
626
+ linkContext: getLinkContext(link),
627
+ linkSection: sanitizeText(getNearestHeading(link), 100),
628
+ linkIndex: getLinkIndex(link, href)
629
+ });
630
+ flush();
631
+ }, true);
632
+ }
633
+ function getLinkContext(link) {
634
+ if (link.closest(config.navigationSelector)) return "navigation";
635
+ if (link.closest(config.footerSelector)) return "footer";
636
+ if (link.closest(config.contentSelector)) return "content";
637
+ return "other";
638
+ }
639
+ function getNearestHeading(element) {
640
+ let current = element;
641
+ while (current && current !== document.body) {
642
+ let sibling = current.previousElementSibling;
643
+ while (sibling) {
644
+ if (/^H[1-6]$/.test(sibling.tagName)) return sibling.textContent?.trim().substring(0, 100) ?? null;
645
+ const headings = sibling.querySelectorAll("h1, h2, h3, h4, h5, h6");
646
+ if (headings.length > 0) return headings[headings.length - 1].textContent?.trim().substring(0, 100) ?? null;
647
+ sibling = sibling.previousElementSibling;
648
+ }
649
+ current = current.parentElement;
650
+ }
651
+ return null;
652
+ }
653
+ function getLinkIndex(link, href) {
654
+ if (typeof CSS === "undefined" || typeof CSS.escape !== "function") return 1;
655
+ try {
656
+ const allLinks = document.querySelectorAll("a[href=\"" + CSS.escape(href) + "\"]");
657
+ for (let i = 0; i < allLinks.length; i++) if (allLinks[i] === link) return i + 1;
658
+ } catch {}
659
+ return 1;
660
+ }
661
+ let trackedScrollDepths = /* @__PURE__ */ new Set();
662
+ let scrollContainer = null;
663
+ function findScrollableAncestor(el) {
664
+ let current = el;
665
+ while (current && current !== document.body && current !== document.documentElement) {
666
+ const overflowY = window.getComputedStyle(current).overflowY;
667
+ if ((overflowY === "auto" || overflowY === "scroll") && current.scrollHeight > current.clientHeight) return current;
668
+ current = current.parentElement;
669
+ }
670
+ return null;
671
+ }
672
+ /**
673
+ * Track scroll depth.
674
+ *
675
+ * Some frameworks (GitBook/HonKit, MkDocs Material) use container-based
676
+ * scrolling where the window itself never scrolls. We detect the scrollable
677
+ * container by walking up from the content element and listen on it in
678
+ * addition to the window.
679
+ */
680
+ function setupScrollTracking() {
681
+ if (!config.trackScrollDepth) return;
682
+ if (config.contentSelector) {
683
+ const contentEl = document.querySelector(config.contentSelector);
684
+ if (contentEl) scrollContainer = findScrollableAncestor(contentEl);
685
+ }
686
+ let ticking = false;
687
+ function onScroll() {
688
+ if (!ticking) {
689
+ window.requestAnimationFrame(() => {
690
+ checkScrollDepth();
691
+ ticking = false;
692
+ });
693
+ ticking = true;
694
+ }
695
+ }
696
+ window.addEventListener("scroll", onScroll);
697
+ if (scrollContainer) {
698
+ scrollContainer.addEventListener("scroll", onScroll);
699
+ if (config.debug) {
700
+ const sc = scrollContainer;
701
+ console.log("[do11y] Using container-based scroll tracking:", sc.className || sc.tagName);
702
+ }
703
+ }
704
+ checkScrollDepth();
705
+ }
706
+ /**
707
+ * Check and track scroll depth thresholds.
708
+ * Reads from the detected scroll container when present, otherwise
709
+ * falls back to the window/document.
710
+ *
711
+ * If the page fits entirely in the viewport (no scrollbar), all
712
+ * thresholds are marked as reached since the user can see 100% of
713
+ * the content without scrolling.
714
+ */
715
+ function checkScrollDepth() {
716
+ let scrollTop;
717
+ let totalHeight;
718
+ let viewportHeight;
719
+ if (scrollContainer && scrollContainer.scrollHeight > scrollContainer.clientHeight) {
720
+ scrollTop = scrollContainer.scrollTop;
721
+ totalHeight = scrollContainer.scrollHeight;
722
+ viewportHeight = scrollContainer.clientHeight;
723
+ } else {
724
+ scrollTop = window.scrollY || document.documentElement.scrollTop;
725
+ totalHeight = document.documentElement.scrollHeight;
726
+ viewportHeight = window.innerHeight;
727
+ }
728
+ const docHeight = totalHeight - viewportHeight;
729
+ if (docHeight <= 0) {
730
+ config.scrollThresholds.forEach((threshold) => {
731
+ if (!trackedScrollDepths.has(threshold)) {
732
+ trackedScrollDepths.add(threshold);
733
+ queueEvent("scroll_depth", {
734
+ threshold,
735
+ scrollPercent: 100
736
+ });
737
+ }
738
+ });
739
+ return;
740
+ }
741
+ const scrollPercent = Math.round(scrollTop / docHeight * 100);
742
+ config.scrollThresholds.forEach((threshold) => {
743
+ if (scrollPercent >= threshold && !trackedScrollDepths.has(threshold)) {
744
+ trackedScrollDepths.add(threshold);
745
+ queueEvent("scroll_depth", {
746
+ threshold,
747
+ scrollPercent
748
+ });
749
+ }
750
+ });
751
+ }
752
+ let pageLoadTime = Date.now();
753
+ let lastActivityTime = Date.now();
754
+ let totalActiveTime = 0;
755
+ let isPageVisible = true;
756
+ function emitPageExit() {
757
+ if (isPageVisible) totalActiveTime += Date.now() - lastActivityTime;
758
+ const totalTime = Date.now() - pageLoadTime;
759
+ const engagementRatio = totalTime > 0 ? totalActiveTime / totalTime : 0;
760
+ let maxScroll = 0;
761
+ trackedScrollDepths.forEach((depth) => {
762
+ if (depth > maxScroll) maxScroll = depth;
763
+ });
764
+ flushVisibleSections();
765
+ const session = getSession();
766
+ queueEvent("page_exit", {
767
+ totalTimeSeconds: Math.round(totalTime / 1e3),
768
+ activeTimeSeconds: Math.round(totalActiveTime / 1e3),
769
+ engagementRatio: Math.round(engagementRatio * 100) / 100,
770
+ maxScrollDepth: maxScroll,
771
+ referrerCategory: session.referrerCategory,
772
+ aiPlatform: session.aiPlatform
773
+ });
774
+ }
775
+ function setupEngagementTracking() {
776
+ document.addEventListener("visibilitychange", () => {
777
+ if (document.hidden) {
778
+ if (isPageVisible) {
779
+ totalActiveTime += Date.now() - lastActivityTime;
780
+ isPageVisible = false;
781
+ }
782
+ } else {
783
+ lastActivityTime = Date.now();
784
+ isPageVisible = true;
785
+ }
786
+ });
787
+ window.addEventListener("beforeunload", () => {
788
+ emitPageExit();
789
+ cleanup();
790
+ });
791
+ }
792
+ function setupSearchTracking() {
793
+ document.addEventListener("click", (e) => {
794
+ if (e.target.closest(config.searchSelector)) queueEvent("search_opened", {});
795
+ });
796
+ document.addEventListener("keydown", (e) => {
797
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") queueEvent("search_opened", { trigger: "keyboard" });
798
+ });
799
+ }
800
+ function getCodeBlockIndex(codeBlock) {
801
+ if (!codeBlock) return 1;
802
+ try {
803
+ const allBlocks = document.querySelectorAll(config.codeBlockSelector);
804
+ for (let i = 0; i < allBlocks.length; i++) if (allBlocks[i] === codeBlock) return i + 1;
805
+ } catch {}
806
+ return 1;
807
+ }
808
+ function setupCopyTracking() {
809
+ document.addEventListener("click", (e) => {
810
+ const copyButton = e.target.closest(config.copyButtonSelector);
811
+ if (copyButton) {
812
+ const codeBlock = copyButton.closest(config.codeBlockSelector) ?? copyButton.closest("div, section")?.querySelector("pre") ?? copyButton.parentElement?.querySelector("pre") ?? null;
813
+ const codeEl = codeBlock ? codeBlock.tagName === "PRE" ? codeBlock.querySelector("code") : codeBlock.querySelector("code[class*=\"language-\"]") ?? codeBlock.querySelector("code") : null;
814
+ queueEvent("code_copied", {
815
+ language: codeBlock?.getAttribute("language") ?? codeBlock?.getAttribute("data-language") ?? codeBlock?.getAttribute("data-lang") ?? codeBlock?.className.match(/language-(\w+)/)?.[1] ?? codeEl?.getAttribute("language") ?? codeEl?.getAttribute("data-language") ?? codeEl?.getAttribute("data-lang") ?? codeEl?.className.match(/language-(\w+)/)?.[1] ?? "unknown",
816
+ codeSection: sanitizeText(getNearestHeading(codeBlock ?? copyButton), 100),
817
+ codeBlockIndex: getCodeBlockIndex(codeBlock)
818
+ });
819
+ }
820
+ }, true);
821
+ }
822
+ let sectionObserver = null;
823
+ let sectionTimers = {};
824
+ function setupSectionVisibilityTracking() {
825
+ if (!config.trackSectionVisibility) return;
826
+ if (typeof IntersectionObserver === "undefined") return;
827
+ const threshold = config.sectionVisibleThreshold * 1e3;
828
+ sectionObserver = new IntersectionObserver((entries) => {
829
+ entries.forEach((entry) => {
830
+ const id = entry.target.getAttribute("data-do11y-section-id");
831
+ if (!id) return;
832
+ if (entry.isIntersecting) {
833
+ if (!sectionTimers[id]) sectionTimers[id] = {
834
+ start: Date.now(),
835
+ reported: false
836
+ };
837
+ } else {
838
+ if (sectionTimers[id] && !sectionTimers[id].reported) {
839
+ const elapsed = Date.now() - sectionTimers[id].start;
840
+ if (elapsed >= threshold) {
841
+ queueEvent("section_visible", {
842
+ heading: sanitizeText(entry.target.textContent?.trim() ?? "", 100),
843
+ headingLevel: parseInt(entry.target.tagName.charAt(1), 10),
844
+ visibleSeconds: Math.round(elapsed / 1e3)
845
+ });
846
+ sectionTimers[id].reported = true;
847
+ }
848
+ }
849
+ delete sectionTimers[id];
850
+ }
851
+ });
852
+ }, { threshold: .5 });
853
+ observeHeadings();
854
+ }
855
+ function observeHeadings() {
856
+ if (!sectionObserver) return;
857
+ document.querySelectorAll("h2, h3").forEach((h, i) => {
858
+ h.setAttribute("data-do11y-section-id", "section-" + i);
859
+ sectionObserver.observe(h);
860
+ });
861
+ }
862
+ function flushVisibleSections() {
863
+ if (!sectionObserver) return;
864
+ const now = Date.now();
865
+ const threshold = config.sectionVisibleThreshold * 1e3;
866
+ Object.keys(sectionTimers).forEach((id) => {
867
+ const timer = sectionTimers[id];
868
+ if (timer && !timer.reported) {
869
+ const elapsed = now - timer.start;
870
+ if (elapsed >= threshold) {
871
+ const escapedId = typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(id) : id.replace(/["\\]/g, "\\$&");
872
+ const el = document.querySelector("[data-do11y-section-id=\"" + escapedId + "\"]");
873
+ if (el) queueEvent("section_visible", {
874
+ heading: sanitizeText(el.textContent?.trim() ?? "", 100),
875
+ headingLevel: parseInt(el.tagName.charAt(1), 10),
876
+ visibleSeconds: Math.round(elapsed / 1e3)
877
+ });
878
+ }
879
+ }
880
+ });
881
+ sectionTimers = {};
882
+ }
883
+ function setupTabSwitchTracking() {
884
+ if (!config.trackTabSwitches) return;
885
+ document.addEventListener("click", (e) => {
886
+ let baseSel = "[role=\"tab\"], .tabs button, .tabs a, .tabbed-labels label";
887
+ const safeTabSel = validateSelector(config.tabContainerSelector);
888
+ if (safeTabSel) baseSel += ", " + safeTabSel + " button, " + safeTabSel + " a, " + safeTabSel + " label";
889
+ const tab = e.target.closest(baseSel);
890
+ if (!tab) return;
891
+ if (tab.getAttribute("aria-selected") === "true" || tab.classList.contains("active") || tab.classList.contains("is-active")) return;
892
+ const label = sanitizeText(tab.textContent, 50);
893
+ if (!label) return;
894
+ queueEvent("tab_switch", {
895
+ tabLabel: label,
896
+ tabGroup: sanitizeText(getNearestHeading(tab), 100),
897
+ isDefault: false
898
+ });
899
+ });
900
+ }
901
+ function setupTocClickTracking() {
902
+ if (!config.trackTocClicks) return;
903
+ document.addEventListener("click", (e) => {
904
+ const link = e.target.closest("a");
905
+ if (!link) return;
906
+ const tocContainer = link.closest(validateSelector(config.tocSelector) ?? ".table-of-contents, [class*=\"toc\"], [class*=\"outline\"], [class*=\"TableOfContents\"], [class*=\"page-outline\"]");
907
+ if (!tocContainer) return;
908
+ const href = link.getAttribute("href");
909
+ if (!href || !href.startsWith("#")) return;
910
+ const headingText = sanitizeText(link.textContent, 100);
911
+ let headingLevel = null;
912
+ try {
913
+ const targetId = href.slice(1);
914
+ const targetEl = document.getElementById(targetId);
915
+ if (targetEl && /^H[1-6]$/.test(targetEl.tagName)) headingLevel = parseInt(targetEl.tagName.charAt(1), 10);
916
+ } catch {}
917
+ const tocLinks = tocContainer.querySelectorAll("a[href^=\"#\"]");
918
+ let tocPosition = 1;
919
+ for (let i = 0; i < tocLinks.length; i++) if (tocLinks[i] === link) {
920
+ tocPosition = i + 1;
921
+ break;
922
+ }
923
+ queueEvent("toc_click", {
924
+ heading: headingText,
925
+ headingLevel,
926
+ tocPosition
927
+ });
928
+ }, true);
929
+ }
930
+ function setupFeedbackTracking() {
931
+ if (!config.trackFeedback) return;
932
+ document.addEventListener("click", (e) => {
933
+ const button = e.target.closest("button, [role=\"button\"], a");
934
+ if (!button) return;
935
+ if (!button.closest(validateSelector(config.feedbackSelector) ?? "[class*=\"feedback\"], [class*=\"helpful\"], [class*=\"rating\"], [class*=\"was-this\"], [data-feedback]")) return;
936
+ const buttonText = (button.textContent ?? "").trim().toLowerCase();
937
+ const ariaLabel = (button.getAttribute("aria-label") ?? "").toLowerCase();
938
+ const titleAttr = (button.getAttribute("title") ?? "").toLowerCase();
939
+ const rawDataValue = button.getAttribute("data-value") ?? button.getAttribute("data-md-value") ?? button.getAttribute("data-feedback");
940
+ const dataValue = rawDataValue && /^[\w\s.,!?-]{1,50}$/.test(rawDataValue) ? rawDataValue : null;
941
+ let rating = null;
942
+ if (dataValue) rating = dataValue;
943
+ else if (/\byes\b|👍|thumbs.?up|helpful/i.test(buttonText + " " + ariaLabel + " " + titleAttr)) rating = "yes";
944
+ else if (/\bno\b|👎|thumbs.?down|not.?helpful/i.test(buttonText + " " + ariaLabel + " " + titleAttr)) rating = "no";
945
+ if (!rating) return;
946
+ queueEvent("feedback", { rating });
947
+ });
948
+ }
949
+ function setupExpandCollapseTracking() {
950
+ if (!config.trackExpandCollapse) return;
951
+ document.addEventListener("toggle", (e) => {
952
+ const details = e.target;
953
+ if (details.tagName !== "DETAILS") return;
954
+ const summary = details.querySelector("summary");
955
+ queueEvent("expand_collapse", {
956
+ summary: sanitizeText(summary ? summary.textContent : "", 100),
957
+ action: details.open ? "expand" : "collapse",
958
+ section: sanitizeText(getNearestHeading(details), 100)
959
+ });
960
+ }, true);
961
+ document.addEventListener("click", (e) => {
962
+ const trigger = e.target.closest("[aria-expanded], [class*=\"accordion\"] button, [class*=\"collapsible\"] button");
963
+ if (!trigger) return;
964
+ if (trigger.closest("details")) return;
965
+ if (trigger.closest("nav, [role=\"navigation\"], header")) return;
966
+ const wasExpanded = trigger.getAttribute("aria-expanded") === "true";
967
+ queueEvent("expand_collapse", {
968
+ summary: sanitizeText(trigger.textContent, 100),
969
+ action: wasExpanded ? "collapse" : "expand",
970
+ section: sanitizeText(getNearestHeading(trigger), 100)
971
+ });
972
+ });
973
+ }
974
+ let mutationObserver = null;
975
+ function init() {
976
+ if (window.Do11yConfig && typeof window.Do11yConfig === "object") {
977
+ for (const key in window.Do11yConfig) if (Object.prototype.hasOwnProperty.call(window.Do11yConfig, key) && Object.prototype.hasOwnProperty.call(config, key)) config[key] = window.Do11yConfig[key];
978
+ }
979
+ const metaDestination = document.querySelector("meta[name=\"do11y-destination\"]");
980
+ if (metaDestination) {
981
+ const dest = metaDestination.getAttribute("content");
982
+ if (dest === "supabase" || dest === "http") config.destination = dest;
983
+ }
984
+ const metaUrl = document.querySelector("meta[name=\"do11y-url\"]");
985
+ if (metaUrl) config.supabaseUrl = metaUrl.getAttribute("content") ?? config.supabaseUrl;
986
+ const metaKey = document.querySelector("meta[name=\"do11y-key\"]");
987
+ if (metaKey) config.supabaseKey = metaKey.getAttribute("content") ?? config.supabaseKey;
988
+ const metaTable = document.querySelector("meta[name=\"do11y-table\"]");
989
+ if (metaTable) config.supabaseTable = metaTable.getAttribute("content") ?? config.supabaseTable;
990
+ const metaHttpEndpoint = document.querySelector("meta[name=\"do11y-http-endpoint\"]");
991
+ if (metaHttpEndpoint) config.httpEndpoint = metaHttpEndpoint.getAttribute("content") ?? config.httpEndpoint;
992
+ const metaDebug = document.querySelector("meta[name=\"do11y-debug\"]");
993
+ if (metaDebug && metaDebug.getAttribute("content") === "true") config.debug = true;
994
+ const metaDomains = document.querySelector("meta[name=\"do11y-domains\"]");
995
+ if (metaDomains) {
996
+ const domainsStr = metaDomains.getAttribute("content");
997
+ if (domainsStr) config.allowedDomains = domainsStr.split(",").map((d) => d.trim());
998
+ }
999
+ const metaFramework = document.querySelector("meta[name=\"do11y-framework\"]");
1000
+ if (metaFramework) config.framework = metaFramework.getAttribute("content") ?? config.framework;
1001
+ applyFrameworkSelectors();
1002
+ if (config.debug) console.log("[Do11y] Initializing with config:", {
1003
+ destination: config.destination,
1004
+ hasCredentials: config.destination === "supabase" ? !!config.supabaseKey : !!config.httpEndpoint,
1005
+ framework: config.framework,
1006
+ allowedDomains: config.allowedDomains,
1007
+ respectDNT: config.respectDNT
1008
+ });
1009
+ if (shouldDisableTracking()) {
1010
+ isDisabled = true;
1011
+ if (config.debug) console.log("[Do11y] Tracking disabled");
1012
+ return;
1013
+ }
1014
+ if (!(config.destination === "supabase" ? !!config.supabaseKey : !!config.httpEndpoint)) {
1015
+ if (config.debug) {
1016
+ console.warn("[Do11y] No destination configured. Events will not be sent.");
1017
+ console.warn("[Do11y] Add <meta name=\"do11y-url\"> and <meta name=\"do11y-key\"> to enable.");
1018
+ }
1019
+ }
1020
+ trackPageView();
1021
+ setupLinkTracking();
1022
+ setupScrollTracking();
1023
+ setupEngagementTracking();
1024
+ setupSearchTracking();
1025
+ setupCopyTracking();
1026
+ setupSectionVisibilityTracking();
1027
+ setupTabSwitchTracking();
1028
+ setupTocClickTracking();
1029
+ setupFeedbackTracking();
1030
+ setupExpandCollapseTracking();
1031
+ let lastPath = window.location.pathname;
1032
+ mutationObserver = new MutationObserver(() => {
1033
+ if (window.location.pathname !== lastPath) {
1034
+ lastPath = window.location.pathname;
1035
+ emitPageExit();
1036
+ trackedScrollDepths = /* @__PURE__ */ new Set();
1037
+ pageLoadTime = Date.now();
1038
+ lastActivityTime = Date.now();
1039
+ totalActiveTime = 0;
1040
+ isPageVisible = true;
1041
+ trackPageView();
1042
+ observeHeadings();
1043
+ checkScrollDepth();
1044
+ }
1045
+ });
1046
+ mutationObserver.observe(document.body, {
1047
+ childList: true,
1048
+ subtree: true
1049
+ });
1050
+ window.addEventListener("popstate", () => {
1051
+ if (window.location.pathname !== lastPath) {
1052
+ lastPath = window.location.pathname;
1053
+ emitPageExit();
1054
+ trackedScrollDepths = /* @__PURE__ */ new Set();
1055
+ pageLoadTime = Date.now();
1056
+ lastActivityTime = Date.now();
1057
+ totalActiveTime = 0;
1058
+ isPageVisible = true;
1059
+ trackPageView();
1060
+ observeHeadings();
1061
+ checkScrollDepth();
1062
+ }
1063
+ });
1064
+ Object.freeze(config);
1065
+ if (config.debug) console.log("[Do11y] Initialized successfully");
1066
+ }
1067
+ function cleanup() {
1068
+ if (mutationObserver) {
1069
+ mutationObserver.disconnect();
1070
+ mutationObserver = null;
1071
+ }
1072
+ if (sectionObserver) {
1073
+ flushVisibleSections();
1074
+ sectionObserver.disconnect();
1075
+ sectionObserver = null;
1076
+ }
1077
+ if (flushTimeout) {
1078
+ clearTimeout(flushTimeout);
1079
+ flushTimeout = null;
1080
+ }
1081
+ flushSync();
1082
+ }
1083
+ if (!_alreadyLoaded) if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
1084
+ else init();
1085
+ window.Do11y = window.Do11y ?? {
1086
+ getConfig: () => ({
1087
+ destination: config.destination,
1088
+ hasCredentials: config.destination === "supabase" ? !!config.supabaseKey : !!config.httpEndpoint,
1089
+ isDisabled,
1090
+ allowedDomains: config.allowedDomains,
1091
+ respectDNT: config.respectDNT
1092
+ }),
1093
+ flush,
1094
+ isEnabled: () => !isDisabled && (config.destination === "supabase" ? !!config.supabaseKey : !!config.httpEndpoint),
1095
+ getQueueSize: () => eventQueue.length,
1096
+ version: VERSION
1097
+ };
1098
+ //#endregion
1099
+ })();