UNPKG

vue3-game-analytics

Version:

Comprehensive analytics tracking system for Vue 3 game applications

915 lines (914 loc) 29.2 kB
import { reactive as R, computed as y, readonly as L, unref as P } from "vue"; var _ = /* @__PURE__ */ ((e) => (e.BUFFER_OVERFLOW = "BUFFER_OVERFLOW", e.API_SUBMISSION_FAILED = "API_SUBMISSION_FAILED", e.INVALID_EVENT = "INVALID_EVENT", e.CONFIGURATION_ERROR = "CONFIGURATION_ERROR", e.CONSENT_REQUIRED = "CONSENT_REQUIRED", e.DO_NOT_TRACK = "DO_NOT_TRACK", e))(_ || {}); function E(e, t = 100) { let o, u, i = 0; function c(...s) { const r = Date.now(), a = t - (r - i); return a <= 0 || a > t ? (o && (clearTimeout(o), o = void 0), i = r, u = e.apply(this, s)) : o || (o = setTimeout(() => { i = Date.now(), o = void 0, u = e.apply(this, s); }, a)), u; } return c; } function F(e = 1e3) { let t = 0, o = performance.now(), u = 0; function i() { t++; const c = performance.now(), s = c - o; s >= e && (u = Math.round(t * 1e3 / s), t = 0, o = c), requestAnimationFrame(i); } return requestAnimationFrame(i), function() { return u; }; } function $(e, t = 2e3) { typeof window < "u" && "requestIdleCallback" in window ? window.requestIdleCallback(e, { timeout: t }) : setTimeout(e, 1); } function M() { return typeof window > "u" || typeof navigator > "u" ? {} : { deviceType: H(), browser: z(), os: G(), screenResolution: X(), orientation: Y(), connectionType: N(), language: navigator.language || "unknown", userAgent: navigator.userAgent, doNotTrack: Q(), timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }; } function H() { const e = navigator.userAgent; return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(e) ? window.innerWidth > 767 || /iPad/i.test(e) ? "tablet" : "mobile" : "desktop"; } function z() { var u, i, c, s, r, a; const e = navigator.userAgent; let t = "unknown", o = "unknown"; return /Chrome/.test(e) && !/Chromium|Edge|Edg|OPR|Opera/.test(e) ? (t = "Chrome", o = ((u = e.match(/Chrome\/(\d+\.\d+)/)) == null ? void 0 : u[1]) || "") : /Firefox/.test(e) ? (t = "Firefox", o = ((i = e.match(/Firefox\/(\d+\.\d+)/)) == null ? void 0 : i[1]) || "") : /Safari/.test(e) && !/Chrome|Chromium|Edge|Edg|OPR|Opera/.test(e) ? (t = "Safari", o = ((c = e.match(/Version\/(\d+\.\d+)/)) == null ? void 0 : c[1]) || "") : /Edge|Edg/.test(e) ? (t = "Edge", o = ((s = e.match(/Edge\/(\d+\.\d+)|Edg\/(\d+\.\d+)/)) == null ? void 0 : s[1]) || "") : /OPR|Opera/.test(e) ? (t = "Opera", o = ((r = e.match(/OPR\/(\d+\.\d+)|Opera\/(\d+\.\d+)/)) == null ? void 0 : r[1]) || "") : /MSIE|Trident/.test(e) && (t = "Internet Explorer", o = ((a = e.match(/MSIE (\d+\.\d+)|rv:(\d+\.\d+)/)) == null ? void 0 : a[1]) || ""), `${t} ${o}`.trim(); } function G() { var o, u, i, c, s, r; const e = navigator.userAgent; let t = "unknown"; if (/Windows NT/.test(e)) { t = "Windows"; const a = (o = e.match(/Windows NT (\d+\.\d+)/)) == null ? void 0 : o[1]; if (a) switch (a) { case "10.0": t += " 10"; break; case "6.3": t += " 8.1"; break; case "6.2": t += " 8"; break; case "6.1": t += " 7"; break; case "6.0": t += " Vista"; break; case "5.2": t += " XP"; break; case "5.1": t += " XP"; break; default: t += ` ${a}`; break; } } else if (/Mac OS X/.test(e)) { t = "macOS"; const a = (i = (u = e.match(/Mac OS X (\d+[._]\d+)/)) == null ? void 0 : u[1]) == null ? void 0 : i.replace("_", "."); a && (t += ` ${a}`); } else if (/iPhone|iPad|iPod/.test(e)) { t = "iOS"; const a = (s = (c = e.match(/OS (\d+[._]\d+)/)) == null ? void 0 : c[1]) == null ? void 0 : s.replace("_", "."); a && (t += ` ${a}`); } else if (/Android/.test(e)) { t = "Android"; const a = (r = e.match(/Android (\d+\.\d+)/)) == null ? void 0 : r[1]; a && (t += ` ${a}`); } else /Linux/.test(e) && (t = "Linux"); return t; } function X() { if (typeof screen > "u") return "unknown"; const e = screen.width || window.innerWidth, t = screen.height || window.innerHeight; return `${e}x${t}`; } function Y() { return typeof window > "u" || typeof screen > "u" ? "unknown" : screen.orientation ? screen.orientation.type.includes("portrait") ? "portrait" : "landscape" : window.innerHeight > window.innerWidth ? "portrait" : "landscape"; } function N() { const e = navigator.connection || navigator.mozConnection || navigator.webkitConnection; return e && (e.effectiveType || e.type) || "unknown"; } function Q() { return navigator.doNotTrack ? navigator.doNotTrack === "1" ? "yes" : "no" : navigator.msDoNotTrack ? navigator.msDoNotTrack === "1" ? "yes" : "no" : window.doNotTrack ? window.doNotTrack === "1" ? "yes" : "no" : "unspecified"; } function U(e) { const t = () => { e({ isOnline: navigator.onLine, connectionType: N() }); }; window.addEventListener("online", t), window.addEventListener("offline", t); const o = navigator.connection || navigator.mozConnection || navigator.webkitConnection; return o && o.addEventListener("change", t), t(), () => { window.removeEventListener("online", t), window.removeEventListener("offline", t), o && o.removeEventListener("change", t); }; } function O() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(e) { const t = Math.random() * 16 | 0; return (e === "x" ? t : t & 3 | 8).toString(16); }); } function I(e, t) { const o = {}; if (!e || !e.attributes) return o; for (let u = 0; u < e.attributes.length; u++) { const i = e.attributes[u]; if (i.name.startsWith("data-")) { const c = i.name.substring(5); if (c.startsWith(t + "-")) { const s = c.substring(t.length + 1); o[s] = i.value; } } } return o; } function W(e) { const t = /* @__PURE__ */ new WeakSet(); return JSON.stringify(e, (o, u) => { if (typeof u == "function") return "[Function]"; if (u instanceof Error) return { message: u.message, stack: u.stack, name: u.name }; if (u instanceof HTMLElement) return `[HTMLElement ${u.tagName.toLowerCase()}]`; if (typeof u == "symbol") return u.toString(); if (typeof u == "object" && u !== null) { if (t.has(u)) return "[Circular]"; t.add(u); } return u; }); } function q(e, t) { try { const o = W(t); return localStorage.setItem(e, o), !0; } catch (o) { return console.error("Failed to persist data to storage:", o), !1; } } function B(e) { try { const t = localStorage.getItem(e); return t === null ? null : JSON.parse(t); } catch (t) { return console.error("Failed to retrieve data from storage:", t), null; } } function V(e) { return e >= 1 ? !0 : e <= 0 ? !1 : Math.random() <= e; } const p = "vue3-game-analytics-events", K = "vue3-game-analytics-config", C = { batchSize: 50, flushInterval: 6e4, // 1 minute maxQueueSize: 1e3, trackClicks: !0, trackTouches: !0, trackKeyboard: !0, trackErrors: !0, trackNetwork: !0, trackPerformance: !1, sampleRate: 1, collectEnvironmentData: !0, throttleHighFrequencyEvents: !0, throttleInterval: 100, debug: !1, verboseLogging: !1, visibleTracking: !1, anonymizeIp: !0, respectDoNotTrack: !0, consentRequired: !0 }, j = { capacity: 1e3 }, n = R({ events: [], config: { apiEndpoint: "", gameId: "", playId: "", ...C }, isEnabled: !1, isDebugMode: !1, hasConsent: !1, bufferInfo: { capacity: j.capacity, size: 0, overflowCount: 0 }, networkStatus: { isOnline: !0, lastConnectionType: "unknown" }, pendingFlush: !1, failedSubmissions: 0, lastFlushTime: null }), x = { /** * Check if tracking is currently allowed based on settings and user preferences */ isTrackingAllowed: y(() => !(!n.isEnabled || n.config.consentRequired && !n.hasConsent || n.config.respectDoNotTrack && (navigator.doNotTrack === "1" || // @ts-ignore: Non-standard property window.doNotTrack === "1" || // @ts-ignore: Non-standard property navigator.msDoNotTrack === "1"))), /** * Get the number of events in the queue */ eventCount: y(() => n.events.length), /** * Check if we should be showing debug information */ shouldShowDebugInfo: y(() => n.isDebugMode || !!n.config.debug || !!n.config.visibleTracking) }, g = { /** * Initialize the store with configuration * @param options Configuration options */ initialize(e) { n.config = { ...C, ...e }, n.isDebugMode = !!n.config.debug; const t = B(p); t && Array.isArray(t) && (n.events = t, n.bufferInfo.size = t.length, g.logDebug(`Loaded ${t.length} events from storage`)), n.isEnabled = !0, n.config.flushInterval && n.config.flushInterval > 0 && setInterval(() => { n.events.length > 0 && g.flushEvents(); }, n.config.flushInterval), n.networkStatus.isOnline = navigator.onLine, typeof window < "u" && window.addEventListener("beforeunload", () => { g.saveEventsToStorage(), n.events.length > 0 && navigator.onLine && g.flushEvents(); }), g.logDebug("Analytics store initialized", n.config); }, /** * Set user consent for tracking * @param hasConsent Whether user has given consent */ setConsent(e) { n.hasConsent = e, g.logDebug(`User consent set to: ${e}`), e || (g.clearEvents(), localStorage.removeItem(p), localStorage.removeItem(K)); }, /** * Enable or disable debug mode * @param enabled Whether debug mode should be enabled */ setDebugMode(e) { n.isDebugMode = e, g.logDebug(`Debug mode ${e ? "enabled" : "disabled"}`); }, /** * Update network status * @param status Network status information */ updateNetworkStatus(e) { const t = !n.networkStatus.isOnline; n.networkStatus = { isOnline: e.isOnline, lastConnectionType: e.connectionType }, t && e.isOnline && n.events.length > 0 && g.flushEvents(), g.logDebug("Network status updated", e); }, /** * Track a new analytics event * @param event Event data to track */ trackEvent(e) { if (!x.isTrackingAllowed.value) { g.logDebug("Event not tracked: tracking not allowed", e); return; } if (n.config.sampleRate !== void 0 && n.config.sampleRate < 1 && !V(n.config.sampleRate)) { g.logDebug("Event sampled out", e); return; } const t = { id: O(), timestamp: Date.now(), type: e.type || "custom", gameId: e.gameId || n.config.gameId, playId: e.playId || n.config.playId, ...e }; if (n.config.collectEnvironmentData && !t.environmentData && (t.environmentData = M()), n.config.eventTransformer) try { const o = n.config.eventTransformer(t); g.addEventToQueue(o); } catch (o) { console.error("Error in event transformer:", o), g.addEventToQueue(t); } else g.addEventToQueue(t); }, /** * Add event to the queue and handle queue management * @param event Event to add */ addEventToQueue(e) { n.events.push(e), n.bufferInfo.size = n.events.length, g.logDebug("Event tracked", e), n.config.maxQueueSize && n.events.length > n.config.maxQueueSize && (n.events = n.events.slice(-n.config.maxQueueSize), n.bufferInfo.size = n.events.length, n.bufferInfo.overflowCount++, g.logDebug(`Queue overflow: removed oldest events. Current size: ${n.events.length}`)), n.config.batchSize && n.events.length >= n.config.batchSize && g.flushEvents(), $(() => { g.saveEventsToStorage(); }); }, /** * Save events to localStorage for persistence */ saveEventsToStorage() { n.events.length > 0 && (q(p, n.events), g.logDebug(`Saved ${n.events.length} events to storage`)); }, /** * Send all events to the API endpoint * @returns Promise that resolves when all events are sent */ async flushEvents() { if (!(n.events.length === 0 || n.pendingFlush)) { if (!n.networkStatus.isOnline) { g.logDebug("Flush skipped: device is offline"); return; } n.pendingFlush = !0, g.logDebug(`Flushing ${n.events.length} events to ${n.config.apiEndpoint}`); try { const e = [...n.events], t = await fetch(n.config.apiEndpoint, { method: "POST", headers: { "Content-Type": "application/json", "X-Game-ID": n.config.gameId, "X-Play-ID": n.config.playId }, body: JSON.stringify({ events: e }) }); if (!t.ok) throw new Error(`API responded with status ${t.status}`); n.events = n.events.filter((o) => !e.some((u) => u.id === o.id)), n.bufferInfo.size = n.events.length, n.lastFlushTime = Date.now(), localStorage.removeItem(p), g.logDebug(`Successfully sent ${e.length} events`); } catch (e) { n.failedSubmissions++, console.error("Failed to send events:", e), n.config.trackErrors && g.trackEvent({ type: "error", target: "analytics", error: { message: e instanceof Error ? e.message : String(e), code: _.API_SUBMISSION_FAILED } }); } finally { n.pendingFlush = !1; } } }, /** * Clear all events from the queue */ clearEvents() { n.events = [], n.bufferInfo.size = 0, g.logDebug("Event queue cleared"); }, /** * Filter events in the queue based on criteria * @param filterFn Function to filter events */ filterEvents(e) { const t = n.events.length; n.events = n.events.filter(e), n.bufferInfo.size = n.events.length, g.logDebug(`Filtered events: removed ${t - n.events.length} events`); }, /** * Export all events as JSON * @returns JSON string of all events */ exportEvents() { return JSON.stringify(n.events); }, /** * Log debug messages if debug mode is enabled * @param message Debug message * @param data Optional data to log */ logDebug(e, t) { (n.isDebugMode || n.config.debug) && (n.config.verboseLogging && t ? console.debug(`[GameAnalytics] ${e}`, t) : console.debug(`[GameAnalytics] ${e}`)); } }; let T = null; function h() { return T || (T = { // Expose readonly state ...L(n), // Expose getters ...x, // Expose actions ...g }), T; } function J(e) { const t = h(), o = [], u = e.throttleInterval || 100, i = e.throttleHighFrequencyEvents !== !1; if (e.trackClicks !== !1) { const c = (r) => { if (!r.target) return; const a = r.target, l = { type: "click", target: w(a), coordinates: { x: r.clientX, y: r.clientY, screenX: r.screenX, screenY: r.screenY }, elementData: { type: a.tagName.toLowerCase(), attributes: I(a, "game") } }; t.trackEvent(l), t.shouldShowDebugInfo && e.visibleTracking && D(r.clientX, r.clientY); }, s = i ? E(c, u) : c; document.addEventListener("click", s), o.push({ element: document, type: "click", handler: s }); } if (e.trackTouches !== !1) { const c = (l) => { if (!l.target || !l.touches.length) return; const d = l.touches[0], f = l.target, m = { type: "touch_start", target: w(f), coordinates: { x: d.clientX, y: d.clientY, screenX: d.screenX, screenY: d.screenY }, elementData: { type: f.tagName.toLowerCase(), attributes: I(f, "game") } }; t.trackEvent(m), t.shouldShowDebugInfo && e.visibleTracking && D(d.clientX, d.clientY); }, s = (l) => { if (!l.target || !l.changedTouches.length) return; const d = l.changedTouches[0], f = l.target, m = { type: "touch_end", target: w(f), coordinates: { x: d.clientX, y: d.clientY, screenX: d.screenX, screenY: d.screenY }, elementData: { type: f.tagName.toLowerCase(), attributes: I(f, "game") } }; t.trackEvent(m); }, r = i ? E(c, u) : c, a = i ? E(s, u) : s; document.addEventListener("touchstart", r), document.addEventListener("touchend", a), o.push({ element: document, type: "touchstart", handler: r }), o.push({ element: document, type: "touchend", handler: a }); } if (e.trackKeyboard !== !1) { const c = (r) => { if (["Control", "Shift", "Alt", "Meta"].includes(r.key) || r.repeat) return; const a = r.target, l = { type: "keydown", target: w(a), elementData: { type: a.tagName.toLowerCase(), key: r.key, keyCode: r.keyCode } }; t.trackEvent(l); }, s = i ? E(c, u) : c; document.addEventListener("keydown", s), o.push({ element: document, type: "keydown", handler: s }); } if (e.trackErrors !== !1) { const c = (r) => { var l; const a = { type: "error", error: { message: r.message, stack: (l = r.error) == null ? void 0 : l.stack } }; t.trackEvent(a); }; window.addEventListener("error", c), o.push({ element: window, type: "error", handler: c }); const s = (r) => { var l, d; const a = { type: "error", error: { message: ((l = r.reason) == null ? void 0 : l.message) || "Unhandled promise rejection", stack: (d = r.reason) == null ? void 0 : d.stack, code: "UNHANDLED_REJECTION" } }; t.trackEvent(a); }; window.addEventListener("unhandledrejection", s), o.push({ element: window, type: "unhandledrejection", handler: s }); } return () => { o.forEach(({ element: c, type: s, handler: r }) => { c.removeEventListener(s, r); }); }; } function w(e) { if (e.id) return `#${e.id}`; const t = e.getAttribute("data-game-id"); if (t) return `[data-game-id="${t}"]`; const o = e.getAttribute("name"); return o ? `[name="${o}"]` : e.className && typeof e.className == "string" ? `.${e.className.trim().replace(/\s+/g, ".")}` : e.tagName.toLowerCase(); } function D(e, t) { const o = document.createElement("div"); o.style.position = "absolute", o.style.width = "20px", o.style.height = "20px", o.style.borderRadius = "50%", o.style.backgroundColor = "rgba(255, 0, 0, 0.5)", o.style.left = `${e - 10}px`, o.style.top = `${t - 10}px`, o.style.pointerEvents = "none", o.style.zIndex = "10000", o.style.transform = "scale(0)", o.style.transition = "transform 0.2s ease-out, opacity 0.5s ease-out", document.body.appendChild(o), setTimeout(() => { o.style.transform = "scale(1)"; }, 10), setTimeout(() => { o.style.opacity = "0", setTimeout(() => { o.parentNode && o.parentNode.removeChild(o); }, 500); }, 1e3); } const Z = { install(e, t) { if (!t.apiEndpoint) { console.error("[GameAnalytics] apiEndpoint is required"); return; } if (!t.gameId) { console.error("[GameAnalytics] gameId is required"); return; } t.playId || (t.playId = O()), e.config.globalProperties._gameAnalyticsOptions = t, e.config.globalProperties._eventQueue = [], e.config.globalProperties._analyticsReady = !1; let o, u, i; const c = e.mount; e.mount = function(r) { const a = c.call(this, r); try { const l = h(); l.initialize(t), o = J(t), u = U((m) => { l.updateNetworkStatus(m); }); let d; t.trackPerformance && (d = F(), i = setInterval(() => { const m = d ? d() : 0; l.trackEvent({ type: "performance", metadata: { fps: m, // @ts-expect-error: Non-standard property memoryUsage: window.performance && window.performance.memory ? ( // @ts-expect-error: Non-standard property Math.round(window.performance.memory.usedJSHeapSize / (1024 * 1024)) ) : void 0 } }); }, 3e4)), e.config.globalProperties._consentSetting !== void 0 && (l.setConsent(e.config.globalProperties._consentSetting), delete e.config.globalProperties._consentSetting); const f = e.config.globalProperties._eventQueue; f && f.length > 0 && (f.forEach((m) => { l.trackEvent(m); }), e.config.globalProperties._eventQueue = []), e.config.globalProperties._analyticsReady = !0; } catch (l) { console.error("[GameAnalytics] Failed to initialize analytics store:", l), e.config.globalProperties._analyticsReady = !0; } return a; }, e.config.globalProperties.$gameAnalytics = { trackEvent: (r) => { if (e.config.globalProperties._analyticsReady) try { h().trackEvent(r); } catch (a) { console.error("[GameAnalytics] Error tracking event:", a); } else e.config.globalProperties._eventQueue.push(r); }, flushEvents: () => { if (e.config.globalProperties._analyticsReady) try { h().flushEvents(); } catch (r) { console.error("[GameAnalytics] Error flushing events:", r); } }, enableDebug: () => { if (e.config.globalProperties._analyticsReady) try { h().setDebugMode(!0); } catch (r) { console.error("[GameAnalytics] Error enabling debug mode:", r); } }, disableDebug: () => { if (e.config.globalProperties._analyticsReady) try { h().setDebugMode(!1); } catch (r) { console.error("[GameAnalytics] Error disabling debug mode:", r); } }, clearEvents: () => { if (e.config.globalProperties._analyticsReady) try { h().clearEvents(); } catch (r) { console.error("[GameAnalytics] Error clearing events:", r); } e.config.globalProperties._eventQueue = []; }, getEventCount: () => { if (e.config.globalProperties._analyticsReady) try { return h().eventCount; } catch (r) { return console.error("[GameAnalytics] Error getting event count:", r), 0; } return e.config.globalProperties._eventQueue.length; }, setConsent: (r) => { if (e.config.globalProperties._analyticsReady) try { h().setConsent(r); } catch (a) { console.error("[GameAnalytics] Error setting consent:", a); } else e.config.globalProperties._consentSetting = r; } }; const s = e.unmount; e.unmount = function() { try { if (o && o(), u && u(), i && clearInterval(i), e.config.globalProperties._analyticsReady) try { h().flushEvents(); } catch (r) { console.error("[GameAnalytics] Error flushing events during unmount:", r); } } catch (r) { console.error("[GameAnalytics] Error during cleanup:", r); } s.call(this); }; } }; function ee() { const e = h(), t = /* @__PURE__ */ new WeakMap(), o = (i, c, s) => { const r = s.value || {}, a = r.event || s.arg || "click", l = r.target || i.getAttribute("data-track-id") || te(i), d = r.context || {}; let f = { type: a, target: l, elementData: { type: i.tagName.toLowerCase(), attributes: I(i, "game") }, metadata: d }; if (c instanceof MouseEvent) f.coordinates = { x: c.clientX, y: c.clientY, screenX: c.screenX, screenY: c.screenY }; else if (c instanceof TouchEvent && c.touches.length > 0) { const m = c.touches[0]; f.coordinates = { x: m.clientX, y: m.clientY, screenX: m.screenX, screenY: m.screenY }; } e.trackEvent(f); }, u = (i, c, s, r) => { var f; const a = ((f = s.value) == null ? void 0 : f.throttle) || 300; let l = t.get(i); l || (l = {}, t.set(i, l)); const d = `${c}_${a}`; return l[d] || (l[d] = E(r, a)), l[d]; }; return { // When directive is mounted to an element mounted(i, c) { (c.arg ? [c.arg] : ["click"]).forEach((r) => { const l = u(i, r, c, (f) => o(i, f, c)); i.addEventListener(r, l); const d = i._trackHandlers || {}; d[r] = l, i._trackHandlers = d; }); }, // When directive parameters change updated(i, c) { i._trackHandlers && Object.entries(i._trackHandlers).forEach(([a, l]) => { i.removeEventListener(a, l); }); const s = c.arg ? [c.arg] : ["click"], r = {}; s.forEach((a) => { const d = u(i, a, c, (f) => o(i, f, c)); i.addEventListener(a, d), r[a] = d; }), i._trackHandlers = r; }, // When directive is removed unmounted(i) { i._trackHandlers && (Object.entries(i._trackHandlers).forEach(([c, s]) => { i.removeEventListener(c, s); }), delete i._trackHandlers), t.delete(i); } }; } function te(e) { if (e.id) return `#${e.id}`; const t = e.getAttribute("data-game-id"); if (t) return `[data-game-id="${t}"]`; if (e.className && typeof e.className == "string") { const o = e.className.trim().split(/\s+/); if (o.length > 0) return `.${o.join(".")}`; } return e.tagName.toLowerCase(); } function ne(e) { e.directive("track", ee()); } function oe() { const e = h(); function t(s) { e.trackEvent(s); } function o(s, r = {}) { const { type: a = "interaction", throttleInterval: l = 100, metadata: d = {} } = r; return E((m, A = {}) => { const v = P(s); if (!v) return; const k = v.getBoundingClientRect(); e.trackEvent({ type: m || a, target: v.id ? `#${v.id}` : v.tagName.toLowerCase(), coordinates: { x: k.left + k.width / 2, y: k.top + k.height / 2 }, elementData: { type: v.tagName.toLowerCase(), state: v.getAttribute("data-state") || void 0, attributes: Array.from(v.attributes).filter((b) => b.name.startsWith("data-game-")).reduce((b, S) => (b[S.name.replace("data-game-", "")] = S.value, b), {}) }, metadata: { ...d, ...A } }); }, l); } function u(s) { const r = {}; return { /** * Start tracking a timed interaction * @param metadata Additional metadata for the start event */ start: (a = {}) => { r[s] = Date.now(), e.trackEvent({ type: "interaction_start", target: s, metadata: { interactionId: s, ...a } }); }, /** * End tracking a timed interaction and calculate duration * @param metadata Additional metadata for the end event * @returns Duration in milliseconds, or undefined if start wasn't called */ end: (a = {}) => { const l = r[s]; if (!l) { console.warn(`[GameAnalytics] No start time found for timed interaction "${s}"`); return; } const d = Date.now() - l; return delete r[s], e.trackEvent({ type: "interaction_end", target: s, duration: d, metadata: { interactionId: s, ...a } }), d; }, /** * Cancel tracking a timed interaction * @param reason Optional reason for cancellation */ cancel: (a) => { const l = r[s]; if (!l) return; const d = Date.now() - l; delete r[s], e.trackEvent({ type: "interaction_cancelled", target: s, duration: d, metadata: { interactionId: s, reason: a } }); } }; } function i(s) { e.trackEvent({ type: "game_state_change", gameState: s }); } async function c() { return e.flushEvents(); } return { // Core tracking methods trackEvent: t, trackElement: o, trackTimedInteraction: u, trackGameState: i, // Utility methods flushEvents: c, clearEvents: () => e.clearEvents(), // Debug methods enableDebug: () => e.setDebugMode(!0), disableDebug: () => e.setDebugMode(!1), isDebugMode: y(() => e.isDebugMode), // Privacy and consent setConsent: (s) => e.setConsent(s), hasConsent: y(() => e.hasConsent), // Store information eventCount: y(() => e.eventCount), isEnabled: y(() => e.isEnabled), isOnline: y(() => e.networkStatus.isOnline) }; } const ae = { install(e, t) { e.use(Z, t), ne(e); } }; export { _ as AnalyticsErrorCode, ae as default, O as generateUUID, oe as useGameAnalytics };