/*! infralytiqs-sdk v1.0.2 | (c) 2026 sharadtech | License: See LICENSE | Built: 2026-06-07T21:38:38.343Z */
var Infralytiqs = (function (exports) {
    'use strict';

    /**
     * Central environment configuration for the Infralytiqs tracker SDK.
     *
     * These defaults are compile-time constants. They are overridden at runtime
     * via the `Infralytiqs.init({ ... })` bootstrap call on each website.
     */
    const ENV = {
        /** Default analytics API base URL (st-ck-server). Override per-site. */
        ANALYTICS_API_BASE_URL: '',
        /** Ingest endpoint pattern. :tenant_id and :site_id are replaced at runtime. */
        INGEST_PATH: '/il/analytics/:tenant_id/:site_id/events',
        /** Max events to hold before auto-flushing the queue */
        BATCH_SIZE: 20,
        /** Interval (ms) between automatic queue flushes */
        FLUSH_INTERVAL_MS: 5000,
        /** Session inactivity timeout (ms) — 30 minutes */
        SESSION_TIMEOUT_MS: 30 * 60 * 1000,
        /** LocalStorage key prefix for tracker state */
        STORAGE_PREFIX: '_il_',
        /** SDK version — injected into every event payload for debugging */
        SDK_VERSION: '1.0.0',
        /** Maximum retry attempts for failed flushes */
        MAX_RETRIES: 2,
        /** Enable debug logging to browser console (overridden by init config) */
        DEBUG: false,
    };

    const ANON_KEY = `${ENV.STORAGE_PREFIX}anon_id`;
    const SESSION_KEY = `${ENV.STORAGE_PREFIX}session_id`;
    const SESSION_TS_KEY = `${ENV.STORAGE_PREFIX}session_ts`;
    function uuidv4() {
        if (typeof crypto !== 'undefined' && crypto.randomUUID) {
            return crypto.randomUUID();
        }
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
            const r = (Math.random() * 16) | 0;
            const v = c === 'x' ? r : (r & 0x3) | 0x8;
            return v.toString(16);
        });
    }
    function safeGet(key) {
        try {
            return localStorage.getItem(key);
        }
        catch (_a) {
            return null;
        }
    }
    function safeSet(key, value) {
        try {
            localStorage.setItem(key, value);
        }
        catch ( /* storage unavailable */_a) { /* storage unavailable */ }
    }
    /**
     * Returns a persistent anonymous visitor ID. Created once and stored in localStorage.
     */
    function getAnonymousId() {
        let id = safeGet(ANON_KEY);
        if (!id) {
            id = uuidv4();
            safeSet(ANON_KEY, id);
        }
        return id;
    }
    /**
     * Returns the current session ID, rotating it when the session has expired.
     */
    function getSessionId(timeoutMs) {
        const now = Date.now();
        const lastTs = parseInt(safeGet(SESSION_TS_KEY) || '0', 10);
        let sessionId = safeGet(SESSION_KEY);
        if (!sessionId || (now - lastTs) > timeoutMs) {
            sessionId = uuidv4();
            safeSet(SESSION_KEY, sessionId);
        }
        safeSet(SESSION_TS_KEY, String(now));
        return sessionId;
    }
    /**
     * Explicitly sets the known user ID. Persisted in sessionStorage so it
     * survives SPA navigations but not new browser sessions.
     */
    let _userId = '';
    function setUserId(id) {
        _userId = id;
        try {
            sessionStorage.setItem(`${ENV.STORAGE_PREFIX}user_id`, id);
        }
        catch ( /* noop */_a) { /* noop */ }
    }
    function getUserId() {
        if (_userId)
            return _userId;
        try {
            _userId = sessionStorage.getItem(`${ENV.STORAGE_PREFIX}user_id`) || '';
        }
        catch ( /* noop */_a) { /* noop */ }
        return _userId;
    }

    /**
     * Detects the device form factor from the User-Agent string.
     * One of: 'mobile' | 'tablet' | 'desktop'.
     */
    function getDeviceType() {
        const ua = navigator.userAgent || '';
        if (/tablet|ipad|playbook|silk/i.test(ua))
            return 'tablet';
        if (/mobile|iphone|ipod|android.*mobile|windows phone|blackberry/i.test(ua))
            return 'mobile';
        return 'desktop';
    }
    /**
     * Detects the operating-system family from the User-Agent string.
     *
     * Returns one of:
     *   'iOS' | 'Android' | 'Windows' | 'macOS' | 'Linux' | 'ChromeOS' | 'Other'
     *
     * Order matters — iPadOS still reports `Macintosh` in the UA but exposes
     * touch points, so we check that ahead of macOS.
     */
    function getDeviceOS() {
        const ua = navigator.userAgent || '';
        const platform = navigator.platform || '';
        const maxTouch = navigator.maxTouchPoints || 0;
        if (/iPhone|iPod/i.test(ua))
            return 'iOS';
        // iPadOS 13+ reports as Mac with touch — disambiguate via maxTouchPoints.
        if (/iPad/i.test(ua) || (/Mac/.test(platform) && maxTouch > 1))
            return 'iOS';
        if (/Android/i.test(ua))
            return 'Android';
        if (/CrOS/i.test(ua))
            return 'ChromeOS';
        if (/Windows/i.test(ua))
            return 'Windows';
        if (/Mac OS X|Macintosh/i.test(ua))
            return 'macOS';
        if (/Linux/i.test(ua))
            return 'Linux';
        return 'Other';
    }
    /**
     * Returns the browser language ISO code (e.g. "en-US", "fr").
     */
    function getLanguage() {
        return navigator.language || navigator.userLanguage || '';
    }
    /**
     * Returns country code from the browser language tag if available.
     * This is a best-effort approximation; server-side GeoIP is more accurate.
     */
    function getCountryFromLanguage() {
        const lang = navigator.language || '';
        const parts = lang.split('-');
        return parts.length > 1 ? parts[1].toUpperCase() : '';
    }

    const UTM_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
    /**
     * Extracts UTM parameters from the current URL's query string.
     * Results are cached in sessionStorage so they persist across SPA navigations.
     */
    function getUtmParams() {
        const data = {};
        const prefix = '_il_utm_';
        try {
            const params = new URLSearchParams(window.location.search);
            let hasNew = false;
            for (const key of UTM_PARAMS) {
                const val = params.get(key);
                if (val) {
                    data[key] = val;
                    sessionStorage.setItem(`${prefix}${key}`, val);
                    hasNew = true;
                }
            }
            if (!hasNew) {
                for (const key of UTM_PARAMS) {
                    const cached = sessionStorage.getItem(`${prefix}${key}`);
                    if (cached)
                        data[key] = cached;
                }
            }
        }
        catch ( /* noop */_a) { /* noop */ }
        return data;
    }

    function buildUrl(cfg) {
        const base = cfg.serverUrl.replace(/\/+$/, '');
        const path = ENV.INGEST_PATH
            .replace(':tenant_id', encodeURIComponent(cfg.tenantId))
            .replace(':site_id', encodeURIComponent(cfg.siteId));
        return `${base}${path}`;
    }
    function log(debug, ...args) {
        if (debug && typeof console !== 'undefined') {
            console.log('[Infralytiqs]', ...args);
        }
    }
    /**
     * Sends events via `fetch`. Returns true on success.
     */
    async function sendViaFetch(cfg, events, retries = ENV.MAX_RETRIES) {
        const url = buildUrl(cfg);
        const body = JSON.stringify(events.length === 1 ? events[0] : events);
        for (let attempt = 0; attempt <= retries; attempt++) {
            try {
                const res = await fetch(url, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body,
                    keepalive: true,
                });
                if (res.ok) {
                    log(cfg.debug, `Flushed ${events.length} event(s) — HTTP ${res.status}`);
                    return true;
                }
                log(cfg.debug, `Flush attempt ${attempt + 1} failed — HTTP ${res.status}`);
            }
            catch (err) {
                log(cfg.debug, `Flush attempt ${attempt + 1} error`, err);
            }
        }
        return false;
    }
    /**
     * Sends events via `navigator.sendBeacon` — fire-and-forget, best for page unload.
     * Returns true if the browser accepted the beacon.
     */
    function sendViaBeacon(cfg, events) {
        if (typeof navigator === 'undefined' || !navigator.sendBeacon)
            return false;
        const url = buildUrl(cfg);
        const body = JSON.stringify(events.length === 1 ? events[0] : events);
        const blob = new Blob([body], { type: 'application/json' });
        const ok = navigator.sendBeacon(url, blob);
        log(cfg.debug, `Beacon ${ok ? 'accepted' : 'rejected'} ${events.length} event(s)`);
        return ok;
    }

    /**
     * Resolves evarMap values from an element.
     */
    function resolveEvars(el, evarMap) {
        var _a;
        const dims = {};
        if (!evarMap)
            return dims;
        for (const [key, resolver] of Object.entries(evarMap)) {
            if (typeof resolver === 'function') {
                dims[key] = resolver(el);
            }
            else if (typeof resolver === 'string') {
                const target = el.closest(resolver) || el.querySelector(resolver);
                if (target) {
                    dims[key] = target.getAttribute(`data-${key}`) || ((_a = target.textContent) === null || _a === void 0 ? void 0 : _a.trim()) || '';
                }
            }
        }
        return dims;
    }
    /**
     * Resolves propMap numeric values from an element.
     */
    function resolveProps(el, propMap) {
        const metrics = {};
        if (!propMap)
            return metrics;
        for (const [key, fn] of Object.entries(propMap)) {
            metrics[key] = fn(el);
        }
        return metrics;
    }
    /**
     * Returns human-readable element descriptor for click tracking.
     */
    function describeElement(el) {
        var _a, _b, _c;
        const dims = {};
        const tag = ((_a = el.tagName) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || '';
        dims['element_tag'] = tag;
        if (el.id)
            dims['element_id'] = el.id;
        const cls = el.className;
        if (typeof cls === 'string' && cls)
            dims['element_class'] = cls.split(/\s+/).slice(0, 3).join(' ');
        if (tag === 'a') {
            const href = el.href;
            if (href)
                dims['link_url'] = href;
            const text = (_b = el.textContent) === null || _b === void 0 ? void 0 : _b.trim();
            if (text)
                dims['link_text'] = text.substring(0, 120);
        }
        else if (tag === 'button' || el.getAttribute('role') === 'button') {
            const text = (_c = el.textContent) === null || _c === void 0 ? void 0 : _c.trim();
            if (text)
                dims['button_text'] = text.substring(0, 120);
        }
        const trackLabel = el.getAttribute('data-il-track');
        if (trackLabel)
            dims['track_label'] = trackLabel;
        return dims;
    }
    /**
     * Installs auto-capture listeners. Returns a teardown function.
     */
    function installAutoCapture(config, emit) {
        const teardowns = [];
        if (!config.disableAutoPageView) {
            emit('page_view', null, { referrer: document.referrer || '' }, {});
            const onPopState = () => {
                emit('page_view', 'spa_navigation', { referrer: document.referrer || '' }, {});
            };
            window.addEventListener('popstate', onPopState);
            teardowns.push(() => window.removeEventListener('popstate', onPopState));
            const origPushState = history.pushState.bind(history);
            history.pushState = function (...args) {
                origPushState(...args);
                emit('page_view', 'spa_navigation', { referrer: document.referrer || '' }, {});
            };
            teardowns.push(() => { history.pushState = origPushState; });
        }
        if (!config.disableAutoClick) {
            const selector = config.clickSelector || 'a, button, [data-il-track], input[type="submit"]';
            const onClick = (e) => {
                const target = e.target;
                if (!target)
                    return;
                const matched = target.closest(selector);
                if (!matched)
                    return;
                const dims = Object.assign(Object.assign({}, describeElement(matched)), resolveEvars(matched, config.evarMap));
                const metrics = resolveProps(matched, config.propMap);
                emit('click', null, dims, metrics);
            };
            document.addEventListener('click', onClick, { capture: true, passive: true });
            teardowns.push(() => document.removeEventListener('click', onClick, true));
        }
        if (!config.disableAutoPageLeave) {
            let pageEnteredAt = Date.now();
            const onVisibilityChange = () => {
                if (document.visibilityState === 'hidden') {
                    const timeSpent = Math.round((Date.now() - pageEnteredAt) / 1000);
                    emit('page_leave', null, {}, { time_on_page_sec: timeSpent });
                }
                else {
                    pageEnteredAt = Date.now();
                }
            };
            document.addEventListener('visibilitychange', onVisibilityChange);
            teardowns.push(() => document.removeEventListener('visibilitychange', onVisibilityChange));
            const onBeforeUnload = () => {
                const timeSpent = Math.round((Date.now() - pageEnteredAt) / 1000);
                emit('page_leave', 'unload', {}, { time_on_page_sec: timeSpent });
            };
            window.addEventListener('beforeunload', onBeforeUnload);
            teardowns.push(() => window.removeEventListener('beforeunload', onBeforeUnload));
        }
        return () => teardowns.forEach((fn) => fn());
    }

    /**
     * Opt-in browser geolocation helper for the Infralytiqs SDK.
     *
     * Behaviour:
     *   • Only ever runs if `InfralytiqsConfig.captureLocation === true`.
     *   • Calls `navigator.geolocation.getCurrentPosition` exactly once per
     *     *authorization state* — meaning:
     *       – Consent granted     → coords cached in localStorage for 7 days
     *                               (browsers already remember the permission,
     *                               so we avoid unnecessary re-prompts).
     *       – Consent denied      → denial flag cached in localStorage for 24h
     *                               (short window so users who change their
     *                               mind in browser settings get picked up).
     *       – Prompt timed out    → NOT cached — we'll retry next page load.
     *   • Never blocks tracking. `requestLocation()` returns a promise that
     *     resolves with cached coords (or null) immediately; the real browser
     *     prompt fires in the background and subsequent events pick up the
     *     coords after the user responds.
     *   • Emits no console noise in production — all failures are swallowed.
     */
    const STORAGE_KEY = 'il_geo_v1';
    const DENY_KEY = 'il_geo_deny_v1';
    const GRANT_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
    const DENY_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
    const GEO_TIMEOUT_MS = 8000;
    const GEO_MAX_AGE_MS = 60 * 60 * 1000; // accept a 1h-old fix
    /** Safe storage accessors — localStorage may be disabled (privacy mode). */
    const storage = () => {
        try {
            if (typeof window !== 'undefined' && window.localStorage) {
                const probe = '__il_geo_probe__';
                window.localStorage.setItem(probe, '1');
                window.localStorage.removeItem(probe);
                return window.localStorage;
            }
        }
        catch (_a) {
            /* Private mode / quota exceeded — treat as no storage. */
        }
        return null;
    };
    const readCachedCoords = () => {
        const s = storage();
        if (!s)
            return null;
        try {
            const raw = s.getItem(STORAGE_KEY);
            if (!raw)
                return null;
            const parsed = JSON.parse(raw);
            if (typeof parsed.latitude === 'number'
                && typeof parsed.longitude === 'number'
                && typeof parsed.capturedAt === 'number'
                && Date.now() - parsed.capturedAt < GRANT_TTL_MS) {
                return parsed;
            }
            s.removeItem(STORAGE_KEY);
        }
        catch (_a) {
            /* corrupted payload — nuke it */
            try {
                s.removeItem(STORAGE_KEY);
            }
            catch ( /* noop */_b) { /* noop */ }
        }
        return null;
    };
    const writeCachedCoords = (coords) => {
        const s = storage();
        if (!s)
            return;
        try {
            s.setItem(STORAGE_KEY, JSON.stringify(coords));
        }
        catch (_a) {
            /* ignore */
        }
    };
    const isDenyCached = () => {
        const s = storage();
        if (!s)
            return false;
        try {
            const raw = s.getItem(DENY_KEY);
            if (!raw)
                return false;
            const ts = Number(raw);
            if (Number.isFinite(ts) && Date.now() - ts < DENY_TTL_MS)
                return true;
            s.removeItem(DENY_KEY);
        }
        catch ( /* ignore */_a) { /* ignore */ }
        return false;
    };
    const markDenied = () => {
        const s = storage();
        if (!s)
            return;
        try {
            s.setItem(DENY_KEY, String(Date.now()));
        }
        catch ( /* ignore */_a) { /* ignore */ }
    };
    /** In-memory state shared across all `getCoords()` callers within a page. */
    let memoryCoords = null;
    let pending = null;
    const prime = () => {
        if (memoryCoords)
            return;
        const cached = readCachedCoords();
        if (cached)
            memoryCoords = cached;
    };
    /**
     * Kicks off the permission prompt (or returns a cached fix immediately).
     *
     * Called once from `InfralytiqsTracker.init()` when `captureLocation: true`.
     * Idempotent — subsequent invocations return the same in-flight promise.
     *
     * @param debug Emit a `[Infralytiqs]` log line on each significant state
     *              transition. Matches the tracker's own log gating.
     */
    const requestLocation = (debug) => {
        var _a;
        if (typeof window === 'undefined' || typeof navigator === 'undefined') {
            return Promise.resolve(null);
        }
        prime();
        if (memoryCoords)
            return Promise.resolve(memoryCoords);
        if (isDenyCached()) {
            if (debug)
                console.log('[Infralytiqs] geolocation previously denied — skipping prompt');
            return Promise.resolve(null);
        }
        if (!('geolocation' in navigator) || typeof ((_a = navigator.geolocation) === null || _a === void 0 ? void 0 : _a.getCurrentPosition) !== 'function') {
            return Promise.resolve(null);
        }
        if (pending)
            return pending;
        pending = new Promise((resolve) => {
            let settled = false;
            const done = (v) => {
                if (settled)
                    return;
                settled = true;
                pending = null;
                resolve(v);
            };
            // Hard timeout guard — some OSes silently never call back if the
            // permission dialog is dismissed without a click (e.g. alt-tabbed away).
            const timer = window.setTimeout(() => {
                if (debug)
                    console.log(`[Infralytiqs] geolocation prompt timed out after ${GEO_TIMEOUT_MS}ms`);
                done(null);
            }, GEO_TIMEOUT_MS + 1000);
            try {
                navigator.geolocation.getCurrentPosition((pos) => {
                    window.clearTimeout(timer);
                    const coords = {
                        latitude: pos.coords.latitude,
                        longitude: pos.coords.longitude,
                        accuracy: pos.coords.accuracy,
                        capturedAt: Date.now(),
                    };
                    memoryCoords = coords;
                    writeCachedCoords(coords);
                    if (debug) {
                        console.log(`[Infralytiqs] geolocation granted (sdk=${ENV.SDK_VERSION}, acc=${Math.round(coords.accuracy)}m)`);
                    }
                    done(coords);
                }, (err) => {
                    window.clearTimeout(timer);
                    // PERMISSION_DENIED = 1 — cache the denial for a day so we don't
                    // spam the prompt on every page load.
                    if (err && err.code === 1) {
                        markDenied();
                        if (debug)
                            console.log('[Infralytiqs] geolocation permission denied');
                    }
                    else if (debug) {
                        console.log(`[Infralytiqs] geolocation error (code=${err === null || err === void 0 ? void 0 : err.code}): ${err === null || err === void 0 ? void 0 : err.message}`);
                    }
                    done(null);
                }, {
                    enableHighAccuracy: false,
                    timeout: GEO_TIMEOUT_MS,
                    maximumAge: GEO_MAX_AGE_MS,
                });
            }
            catch (_a) {
                window.clearTimeout(timer);
                done(null);
            }
        });
        return pending;
    };
    /** Synchronous accessor used on the hot event-capture path. */
    const getCachedCoords = () => {
        if (memoryCoords)
            return memoryCoords;
        prime();
        return memoryCoords;
    };

    /** Hard cap on the pre-init buffer to avoid unbounded memory growth. */
    const PRE_INIT_BUFFER_LIMIT = 100;
    /**
     * Computes the effective page URL for analytics.
     *
     * For hash-router SPAs (e.g. `https://site/#/infralytiqs/reports/abc`) the
     * browser's `location.pathname` is always `/` and the real logical route
     * lives in `location.hash`. The ClickHouse backend stores `page_path` as
     *     page_path String ALIAS path(page_url)
     * which strips the `#…` fragment — so every event would be recorded under
     * the root path and reports like "Pages Visited" collapse to a single row.
     *
     * To keep the SDK zero-config for SPAs, when a hash-router pattern (`#/…`)
     * is detected we promote the hash content into the URL path portion of
     * `page_url` (and merge any query strings from either side). Non-hash
     * sites are untouched, so traditional multi-page apps are unaffected.
     *
     * Examples
     *   in : https://x.com/#/foo/bar?q=1
     *   out: https://x.com/foo/bar?q=1
     *
     *   in : https://x.com/page?a=1#/foo/bar?b=2
     *   out: https://x.com/foo/bar?a=1&b=2
     */
    function buildPageUrl() {
        try {
            const loc = window.location;
            const hash = loc.hash || '';
            if (!hash.startsWith('#/')) {
                return loc.href;
            }
            const hashContent = hash.slice(1); // "/foo/bar" or "/foo/bar?x=1"
            const qIdx = hashContent.indexOf('?');
            const hashPath = qIdx >= 0 ? hashContent.slice(0, qIdx) : hashContent;
            const hashQuery = qIdx >= 0 ? hashContent.slice(qIdx + 1) : '';
            const outerQuery = (loc.search || '').replace(/^\?/, '');
            const mergedQuery = [outerQuery, hashQuery].filter(Boolean).join('&');
            return loc.origin + hashPath + (mergedQuery ? '?' + mergedQuery : '');
        }
        catch (_a) {
            // Any unusual runtime (SSR, worker, deeply frozen window) — fall back
            // to the browser's native href. Never throw out of event capture.
            return typeof window !== 'undefined' && window.location ? window.location.href : '';
        }
    }
    class InfralytiqsTracker {
        constructor() {
            this.queue = [];
            this.flushTimer = null;
            this.teardownAutoCapture = null;
            this.initialized = false;
            /** Buffers track() calls made before init() so early events aren't lost. */
            this.preInitBuffer = [];
            this.preInitUserId = null;
        }
        /**
         * Initializes the tracker. Must be called once before any tracking occurs.
         */
        init(config) {
            var _a, _b;
            if (this.initialized) {
                this.log('Already initialized — ignoring duplicate init()');
                return;
            }
            if (!config.serverUrl || !config.tenantId || !config.siteId) {
                console.error('[Infralytiqs] init() requires serverUrl, tenantId, and siteId');
                return;
            }
            // Merge dbName into globalDimensions so every event is tagged with the
            // ClickHouse DB it was intended for (purely informational — the server
            // resolves the real DB from the moduleConfig matching tenantId+siteId).
            const globalDimensions = Object.assign(Object.assign({}, (config.dbName ? { ch_db_name: config.dbName } : {})), ((_a = config.globalDimensions) !== null && _a !== void 0 ? _a : {}));
            this.config = Object.assign(Object.assign({ batchSize: ENV.BATCH_SIZE, flushIntervalMs: ENV.FLUSH_INTERVAL_MS, sessionTimeoutMs: ENV.SESSION_TIMEOUT_MS, debug: ENV.DEBUG, clickSelector: 'a, button, [data-il-track], input[type="submit"]' }, config), { globalDimensions });
            this.transportCfg = {
                serverUrl: this.config.serverUrl,
                tenantId: this.config.tenantId,
                siteId: this.config.siteId,
                debug: (_b = this.config.debug) !== null && _b !== void 0 ? _b : false,
            };
            // A deferred identify() that ran before init still needs to stick.
            if (this.preInitUserId) {
                setUserId(this.preInitUserId);
                this.preInitUserId = null;
            }
            if (this.config.userId) {
                setUserId(this.config.userId);
            }
            this.startFlushTimer();
            this.teardownAutoCapture = installAutoCapture(this.config, (eventType, eventSubtype, dims, metrics) => {
                this.trackRaw(eventType, eventSubtype, dims, metrics);
            });
            this.initialized = true;
            // Fire-and-forget geolocation consent prompt. The promise is NOT awaited
            // — any events queued before the user responds go out with no precise
            // coords (server still resolves country via IP geo), and once consent
            // lands, every subsequent event picks up the cached lat/lng from
            // localStorage. We explicitly pass `debug` through so granted/denied
            // transitions show up in the browser console when debug mode is on.
            if (this.config.captureLocation) {
                requestLocation(this.config.debug).catch(() => { });
            }
            // Drain anything that arrived during the async bootstrap window.
            if (this.preInitBuffer.length > 0) {
                const buffered = this.preInitBuffer.splice(0);
                this.log(`Draining pre-init buffer (${buffered.length} event(s))`);
                for (const b of buffered) {
                    this.trackRaw(b.eventType, b.eventSubtype, b.dims, b.metrics);
                }
            }
            this.log('Initialized', {
                tenantId: config.tenantId,
                siteId: config.siteId,
                dbName: config.dbName,
                captureLocation: !!config.captureLocation,
            });
        }
        /**
         * Manually track a named event.
         *
         * @example
         *   Infralytiqs.track('search', { search_term: 'blue widget' });
         *   Infralytiqs.track('login', { method: 'google' });
         */
        track(eventType, customDimensions, customMetrics, eventSubtype) {
            if (!this.initialized) {
                // Buffer until init() drains us. Capped so a misconfigured page
                // can't OOM the browser.
                if (this.preInitBuffer.length >= PRE_INIT_BUFFER_LIMIT) {
                    this.preInitBuffer.shift();
                }
                this.preInitBuffer.push({
                    eventType,
                    eventSubtype: eventSubtype !== null && eventSubtype !== void 0 ? eventSubtype : null,
                    dims: customDimensions !== null && customDimensions !== void 0 ? customDimensions : {},
                    metrics: customMetrics !== null && customMetrics !== void 0 ? customMetrics : {},
                });
                return;
            }
            this.trackRaw(eventType, eventSubtype !== null && eventSubtype !== void 0 ? eventSubtype : null, customDimensions !== null && customDimensions !== void 0 ? customDimensions : {}, customMetrics !== null && customMetrics !== void 0 ? customMetrics : {});
        }
        /**
         * Identifies the current user. Subsequent events will include this user_id.
         * Safe to call before init() — the id is cached and applied on init.
         */
        identify(userId) {
            if (!this.initialized) {
                this.preInitUserId = userId;
                return;
            }
            setUserId(userId);
            this.log('User identified', userId);
        }
        /**
         * Forces an immediate flush of the event queue.
         */
        async flush() {
            await this.flushQueue(false);
        }
        /**
         * Tears down listeners and stops the flush timer.
         */
        destroy() {
            this.flushQueue(true);
            if (this.flushTimer) {
                clearInterval(this.flushTimer);
                this.flushTimer = null;
            }
            if (this.teardownAutoCapture) {
                this.teardownAutoCapture();
                this.teardownAutoCapture = null;
            }
            this.initialized = false;
            this.log('Destroyed');
        }
        // ─── internal helpers ─────────────────────────────────
        trackRaw(eventType, eventSubtype, dims, metrics) {
            var _a, _b, _c, _d;
            const utm = getUtmParams();
            const payload = {
                language_iso_code: getLanguage(),
                user_id: getUserId(),
                anonymous_id: getAnonymousId(),
                session_id: getSessionId((_a = this.config.sessionTimeoutMs) !== null && _a !== void 0 ? _a : ENV.SESSION_TIMEOUT_MS),
                event_type: eventType,
                event_subtype: eventSubtype,
                custom_dimensions: Object.assign(Object.assign(Object.assign({}, ((_b = this.config.globalDimensions) !== null && _b !== void 0 ? _b : {})), dims), { sdk_version: ENV.SDK_VERSION }),
                custom_metrics: metrics,
                page_url: buildPageUrl(),
                country_code: getCountryFromLanguage(),
                device_type: getDeviceType(),
                device_os: getDeviceOS(),
                utm_source: (_c = utm.utm_source) !== null && _c !== void 0 ? _c : null,
            };
            if (utm.utm_medium)
                payload.custom_dimensions['utm_medium'] = utm.utm_medium;
            if (utm.utm_campaign)
                payload.custom_dimensions['utm_campaign'] = utm.utm_campaign;
            if (utm.utm_term)
                payload.custom_dimensions['utm_term'] = utm.utm_term;
            if (utm.utm_content)
                payload.custom_dimensions['utm_content'] = utm.utm_content;
            // Attach cached precise coords when the site opted in and the user
            // granted consent. Absent on the pre-consent window and for users who
            // denied — in those cases the server-side IP geo still provides a
            // country-level signal for the heatmap.
            if (this.config.captureLocation) {
                const geo = getCachedCoords();
                if (geo) {
                    payload.latitude = geo.latitude;
                    payload.longitude = geo.longitude;
                    payload.location_accuracy = geo.accuracy;
                }
            }
            this.queue.push(payload);
            this.log('Queued', eventType, eventSubtype, `(${this.queue.length}/${this.config.batchSize})`);
            if (this.queue.length >= ((_d = this.config.batchSize) !== null && _d !== void 0 ? _d : ENV.BATCH_SIZE)) {
                this.flushQueue(false);
            }
        }
        startFlushTimer() {
            var _a;
            const interval = (_a = this.config.flushIntervalMs) !== null && _a !== void 0 ? _a : ENV.FLUSH_INTERVAL_MS;
            this.flushTimer = setInterval(() => this.flushQueue(false), interval);
        }
        /**
         * Flushes the queue. Uses beacon for unload scenarios, fetch otherwise.
         */
        async flushQueue(useBeacon) {
            if (this.queue.length === 0)
                return;
            const batch = this.queue.splice(0, this.queue.length);
            if (useBeacon) {
                sendViaBeacon(this.transportCfg, batch);
                return;
            }
            const ok = await sendViaFetch(this.transportCfg, batch);
            if (!ok) {
                this.log('Flush failed — events dropped', batch.length);
            }
        }
        log(...args) {
            var _a;
            if (((_a = this.config) === null || _a === void 0 ? void 0 : _a.debug) && typeof console !== 'undefined') {
                console.log('[Infralytiqs]', ...args);
            }
        }
    }

    /**
     * Client-bootstrap auto-loader.
     *
     * When the SDK script first executes on a host page, it looks for a
     * `window.IL_CLIENT_BOOTSTRAP_LIB` global declared by the host (in AEM this
     * is injected by the `Infralytiqs.html` Sightly fragment). If present, the
     * SDK fetches that client-specific bootstrap JavaScript from the CDN and
     * appends it to <head>. The bootstrap is then responsible for calling
     * `Infralytiqs.init({...})` with tenant/site configuration and wiring any
     * site-specific event hooks.
     *
     * Path resolution
     * ---------------
     * `IL_CLIENT_BOOTSTRAP_LIB` may be either:
     *   • A fully qualified URL ("https://…/bootstrap.min.js") — used as-is.
     *   • A path starting with "/" (e.g. "/cl/publicis/ps/bootstrap.min.js") —
     *     resolved against the SDK's own origin (the CDN host that served this
     *     script). This lets every client bootstrap live in the same S3 bucket
     *     as the SDK without the host page needing to know the CDN hostname.
     *   • A relative path — resolved against the SDK's own URL.
     *
     * The SDK script URL is captured at module-evaluation time via
     * `document.currentScript`, which is only reliable during the SDK's
     * synchronous IIFE execution. A defensive fallback scans existing <script>
     * tags for one whose src matches the SDK bundle naming convention.
     */
    const SDK_SCRIPT_URL = (() => {
        try {
            if (typeof document === 'undefined')
                return '';
            const current = document.currentScript;
            if (current && current.src)
                return current.src;
            const scripts = document.getElementsByTagName('script');
            for (let i = scripts.length - 1; i >= 0; i--) {
                const src = scripts[i].src;
                if (src && /infralytiqs(\.min)?\.js(\?.*)?$/.test(src))
                    return src;
            }
        }
        catch (_a) {
            /* noop — fall through to empty */
        }
        return '';
    })();
    const BOOTSTRAP_FLAG = '__il_bootstrap_loaded__';
    function resolveBootstrapUrl(rawPath) {
        if (/^https?:\/\//i.test(rawPath))
            return rawPath;
        if (!SDK_SCRIPT_URL)
            return rawPath;
        try {
            return new URL(rawPath, SDK_SCRIPT_URL).href;
        }
        catch (_a) {
            return rawPath;
        }
    }
    /**
     * Injects a <script src="…"> tag for the client bootstrap declared on the
     * host page. Idempotent — a window sentinel guards against double injection
     * if the SDK is somehow evaluated twice.
     */
    function loadClientBootstrap() {
        if (typeof window === 'undefined' || typeof document === 'undefined')
            return;
        const w = window;
        if (w[BOOTSTRAP_FLAG])
            return;
        const lib = w.IL_CLIENT_BOOTSTRAP_LIB;
        if (!lib || typeof lib !== 'string')
            return;
        const absoluteUrl = resolveBootstrapUrl(lib);
        w[BOOTSTRAP_FLAG] = true;
        const s = document.createElement('script');
        s.src = absoluteUrl;
        s.async = true;
        s.setAttribute('data-il-bootstrap', 'client');
        s.onerror = () => {
            if (typeof console !== 'undefined') {
                console.error('[Infralytiqs] failed to load client bootstrap from', absoluteUrl);
            }
        };
        const parent = document.head || document.documentElement;
        if (parent) {
            parent.appendChild(s);
        }
    }

    /**
     * Infralytiqs token sync.
     *
     * The host page (e.g. the AEM Infralytiqs.html HTL fragment) renders the
     * tenant's current Infralytiqs token id into a global:
     *
     *     <script>window.IL_TOKEN = "....";</script>
     *
     * On SDK load we copy that value into sessionStorage so the rest of the page
     * (and any embedded Infralytiqs widgets — e.g. the "Infralytiqs SDK" report
     * snippet) can read a single, tab-scoped source of truth. The token is
     * tab-scoped on purpose: it lives only for the current session/tab, mirroring
     * how the host page re-emits a fresh `window.IL_TOKEN` on every render.
     *
     * Update semantics: if a token is already stored and the markup provides a
     * different (non-empty) value, the stored token is replaced with the markup
     * value so the freshest server-issued token always wins.
     */
    /** sessionStorage key holding the Infralytiqs token id. */
    const TOKEN_STORAGE_KEY = `${ENV.STORAGE_PREFIX}token`;
    /**
     * Reads `window.IL_TOKEN` and stores it in sessionStorage, updating the
     * stored value when the markup token differs from what is already cached.
     *
     * @returns the token value now held in sessionStorage, or `null` when no
     *          valid token is available.
     */
    function syncToken() {
        if (typeof window === 'undefined') {
            return null;
        }
        const w = window;
        const markupToken = typeof w.IL_TOKEN === 'string' ? w.IL_TOKEN.trim() : '';
        let stored = null;
        try {
            stored = sessionStorage.getItem(TOKEN_STORAGE_KEY);
        }
        catch (_a) {
            // sessionStorage unavailable (e.g. privacy mode) — nothing we can do.
            return markupToken || null;
        }
        // No usable markup token: keep whatever is already stored (if anything).
        if (!markupToken) {
            return stored;
        }
        // Store on first sight or whenever the markup token has changed.
        if (stored !== markupToken) {
            try {
                sessionStorage.setItem(TOKEN_STORAGE_KEY, markupToken);
            }
            catch (_b) {
                /* storage write failed — return the markup value regardless */
            }
        }
        return markupToken;
    }
    /**
     * @returns the Infralytiqs token id currently held in sessionStorage, or
     *          `null` if none is stored / storage is unavailable.
     */
    function getStoredToken() {
        try {
            return sessionStorage.getItem(TOKEN_STORAGE_KEY);
        }
        catch (_a) {
            return null;
        }
    }

    /**
     * st-ck-server license module id for Infralytiqs (Companies.productPlans[].module,
     * Users.moduleAndRole[].module). Kept here for documentation and optional host-app use.
     */
    const LICENSE_MODULE_ID = 'infralytiqs.com';

    /**
     * Infralytiqs Client SDK — Entry Point
     *
     * Usage:
     *
     * 1. Include the built script in the page <head>:
     *      <script src="https://cdn.example.com/infralytiqs.min.js"></script>
     *
     * 2. Bootstrap in the <body> with a <script> block:
     *      <script>
     *        Infralytiqs.init({
     *          serverUrl:  'https://api.example.com',
     *          tenantId:   'my-tenant',
     *          siteId:     'my-site',
     *          debug:      true,
     *          evarMap: {
     *            product_id: '[data-product-id]',
     *            button_label: function(el) { return el.textContent.trim(); }
     *          },
     *          propMap: {
     *            price: function(el) { return parseFloat(el.getAttribute('data-price') || '0'); }
     *          }
     *        });
     *      </script>
     *
     * 3. Track custom events anywhere:
     *      <script>
     *        Infralytiqs.track('search', { search_term: 'blue widget' });
     *        Infralytiqs.track('login',  { method: 'google' });
     *        Infralytiqs.identify('user-123');
     *      </script>
     */
    const tracker = new InfralytiqsTracker();
    const Infralytiqs = {
        /**
         * Initialize the tracker. Call once per page load.
         */
        init(config) {
            tracker.init(config);
        },
        /**
         * Track a custom event.
         *
         * @param eventType        - e.g. 'login', 'search', 'purchase', 'share'
         * @param customDimensions - key/value string pairs (eVars)
         * @param customMetrics    - key/value numeric pairs (props)
         * @param eventSubtype     - optional subcategory
         */
        track(eventType, customDimensions, customMetrics, eventSubtype) {
            tracker.track(eventType, customDimensions, customMetrics, eventSubtype);
        },
        /**
         * Identify the current user (e.g. after login).
         * All subsequent events will carry this user_id.
         */
        identify(userId) {
            tracker.identify(userId);
        },
        /**
         * Force-flush the event queue immediately.
         */
        flush() {
            return tracker.flush();
        },
        /**
         * Tear down listeners and stop the flush timer.
         */
        destroy() {
            tracker.destroy();
        },
        /**
         * Re-read `window.IL_TOKEN` and sync it into sessionStorage. Runs once
         * automatically on SDK load; can be called again after the host page
         * refreshes the token global.
         *
         * @returns the token now held in sessionStorage, or `null` if none.
         */
        syncToken() {
            return syncToken();
        },
        /**
         * @returns the Infralytiqs token id currently held in sessionStorage.
         */
        getToken() {
            return getStoredToken();
        },
    };
    // ─── Sync the Infralytiqs token into sessionStorage ─────────────────────
    // The host page renders the tenant's current token id as `window.IL_TOKEN`
    // (see AEM Infralytiqs.html). Copy it into sessionStorage on load so any
    // embedded report widget can pick it up, updating the stored value whenever
    // the markup token changes.
    try {
        syncToken();
    }
    catch (_a) {
        /* never throw out of SDK script-tag evaluation */
    }
    // ─── Auto-load the client-specific bootstrap ────────────────────────────
    // When the SDK script tag executes, look for `window.IL_CLIENT_BOOTSTRAP_LIB`
    // declared by the host page (e.g. by the AEM Infralytiqs.html Sightly
    // fragment) and inject a <script> tag for it. The bootstrap is then
    // responsible for calling `Infralytiqs.init({...})` and wiring up any
    // site-specific event hooks. Resolution against the SDK's own CDN origin
    // is handled inside the loader, so host pages can use absolute-path
    // shortcuts like "/cl/publicis/ps/infralytiqs-bootstrap.min.js".
    try {
        loadClientBootstrap();
    }
    catch (_b) {
        /* never throw out of SDK script-tag evaluation */
    }

    exports.LICENSE_MODULE_ID = LICENSE_MODULE_ID;
    exports.TOKEN_STORAGE_KEY = TOKEN_STORAGE_KEY;
    exports.default = Infralytiqs;

    Object.defineProperty(exports, '__esModule', { value: true });

    return exports;

})({});
