[
  {
    "path": ".github/funding.yml",
    "content": "github: swetrix\nko_fi: andriir\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Tests\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n    types: [opened, labeled, synchronize, ready_for_review]\n\njobs:\n  test:\n    name: 🧪 Unit Tests\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [22.x]\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: 'npm'\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Run tests\n        run: npm test\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n\n# Next.js build output\n.next\n\n# Nuxt.js build / generate output\n.nuxt\n# dist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and *not* Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n.DS_Store\npnpm-lock.yaml\nyarn.lock\n"
  },
  {
    "path": ".prettierrc.cjs",
    "content": "/**\n * @see https://prettier.io/docs/configuration\n * @type {import(\"prettier\").Config}\n */\nconst config = {\n  printWidth: 120, // max 120 chars in line, code is easy to read\n  useTabs: false, // use spaces instead of tabs\n  tabWidth: 2, // \"visual width\" of of the \"tab\"\n  trailingComma: 'all', // add trailing commas in objects, arrays, etc.\n  semi: false, // Only add semicolons at the beginning of lines that may introduce ASI failures\n  singleQuote: true, // '' for stings instead of \"\"\n  bracketSpacing: true, // import { some } ... instead of import {some} ...\n  arrowParens: 'always', // braces even for single param in arrow functions (a) => { }\n  jsxSingleQuote: true, // '' for react props\n  bracketSameLine: false, // pretty JSX\n  endOfLine: 'lf', // 'lf' for linux, 'crlf' for windows, we need to use 'lf' for git\n}\n\nmodule.exports = config\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Andrii Romasiun\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://swetrix.com/assets/logo_white.png\">\n  <img alt=\"\" src=\"https://swetrix.com/assets/logo_blue.png\" height=\"80\">\n</picture>\n<br /><br />\n\n> [!NOTE]\n> This repository has been archived. Development has moved to the [Swetrix monorepo](https://github.com/Swetrix/swetrix) under [`packages/tracker-js`](https://github.com/Swetrix/swetrix/tree/main/packages/tracker-js). Please open issues and pull requests there instead.\n\n[![JSDelivr hits](https://data.jsdelivr.com/v1/package/gh/Swetrix/swetrix-js/badge?style=rounded)](https://data.jsdelivr.com/v1/package/gh/Swetrix/swetrix-js/stats)\n[![Package size](https://img.shields.io/bundlephobia/minzip/swetrix)](https://bundlephobia.com/api/size?package=swetrix)\n[![Contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/swetrix/swetrix-js/issues)\n\n# Swetrix Tracking Script\n\nThis repository contains the analytics script which is used at https://swetrix.com \\\nYou can find the detailed documentation and use cases at our [docs page](https://docs.swetrix.com/).\n\nFeel free to contribute to the source code by opening a pull requests. \\\nFor any questions, you can open an issue ticket, refer to our [FAQs](https://swetrix.com/#faq) page or reach us at contact@swetrix.com\n\nThe latest live versions of the script are located at [jsDelivr](https://swetrix.org/swetrix.js) and [NPM](https://www.npmjs.com/package/swetrix).\n\n# Selfhosted API\nIf you are selfhosting the [Swetrix-API](https://github.com/Swetrix/swetrix-api), be sure to point the `apiUrl` parameter to: `https://yourapiinstance.com/log`\n\n# Donate\nYou can support the project by donating us at https://ko-fi.com/andriir \\\nWe can only run our services by once again asking for your financial support!\n"
  },
  {
    "path": "dist/esnext/Lib.d.ts",
    "content": "export interface LibOptions {\n    /**\n     * When set to `true`, localhost events will be sent to server.\n     */\n    devMode?: boolean;\n    /**\n     * When set to `true`, the tracking library won't send any data to server.\n     * Useful for development purposes when this value is set based on `.env` var.\n     */\n    disabled?: boolean;\n    /**\n     * By setting this flag to `true`, we will not collect ANY kind of data about the user with the DNT setting.\n     */\n    respectDNT?: boolean;\n    /** Set a custom URL of the API server (for selfhosted variants of Swetrix). */\n    apiURL?: string;\n    /**\n     * Optional profile ID for long-term user tracking.\n     * If set, it will be used for all pageviews and events unless overridden per-call.\n     */\n    profileId?: string;\n}\nexport interface TrackEventOptions {\n    /** The custom event name. */\n    ev: string;\n    /** If set to `true`, only 1 event with the same ID will be saved per user session. */\n    unique?: boolean;\n    /** Event-related metadata object with string values. */\n    meta?: {\n        [key: string]: string | number | boolean | null | undefined;\n    };\n    /** Optional profile ID for long-term user tracking. Overrides the global profileId if set. */\n    profileId?: string;\n}\nexport interface IPageViewPayload {\n    lc?: string;\n    tz?: string;\n    ref?: string;\n    so?: string;\n    me?: string;\n    ca?: string;\n    te?: string;\n    co?: string;\n    pg?: string | null;\n    /** Pageview-related metadata object with string values. */\n    meta?: {\n        [key: string]: string | number | boolean | null | undefined;\n    };\n    /** Optional profile ID for long-term user tracking. Overrides the global profileId if set. */\n    profileId?: string;\n}\nexport interface IErrorEventPayload {\n    name: string;\n    message?: string | null;\n    lineno?: number | null;\n    colno?: number | null;\n    filename?: string | null;\n    stackTrace?: string | null;\n    meta?: {\n        [key: string]: string | number | boolean | null | undefined;\n    };\n}\nexport interface IInternalErrorEventPayload extends IErrorEventPayload {\n    lc?: string;\n    tz?: string;\n    pg?: string | null;\n}\ninterface IPerfPayload {\n    dns: number;\n    tls: number;\n    conn: number;\n    response: number;\n    render: number;\n    dom_load: number;\n    page_load: number;\n    ttfb: number;\n}\n/**\n * Options for evaluating feature flags.\n */\nexport interface FeatureFlagsOptions {\n    /**\n     * Optional profile ID for long-term user tracking.\n     * If not provided, an anonymous profile ID will be generated server-side based on IP and user agent.\n     * Overrides the global profileId if set.\n     */\n    profileId?: string;\n}\n/**\n * Options for evaluating experiments.\n */\nexport interface ExperimentOptions {\n    /**\n     * Optional profile ID for long-term user tracking.\n     * If not provided, an anonymous profile ID will be generated server-side based on IP and user agent.\n     * Overrides the global profileId if set.\n     */\n    profileId?: string;\n}\n/**\n * The object returned by `trackPageViews()`, used to stop tracking pages.\n */\nexport interface PageActions {\n    /** Stops the tracking of pages. */\n    stop: () => void;\n}\n/**\n * The object returned by `trackErrors()`, used to stop tracking errors.\n */\nexport interface ErrorActions {\n    /** Stops the tracking of errors. */\n    stop: () => void;\n}\nexport interface PageData {\n    /** Current URL path. */\n    path: string;\n    /** The object returned by `trackPageViews()`, used to stop tracking pages. */\n    actions: PageActions;\n}\nexport interface ErrorOptions {\n    /**\n     * A number that indicates how many errors should be sent to the server.\n     * Accepts values between 0 and 1. For example, if set to 0.5 - only ~50% of errors will be sent to Swetrix.\n     * For testing, we recommend setting this value to 1. For production, you should configure it depending on your needs as each error event counts towards your plan.\n     *\n     * The default value for this option is 1.\n     */\n    sampleRate?: number;\n    /**\n     * Callback to edit / prevent sending errors.\n     *\n     * @param payload - The error payload.\n     * @returns The edited payload or `false` to prevent sending the error event. If `true` is returned, the payload will be sent as-is.\n     */\n    callback?: (payload: IInternalErrorEventPayload) => Partial<IInternalErrorEventPayload> | boolean;\n}\nexport interface PageViewsOptions {\n    /**\n     * If set to `true`, only unique events will be saved.\n     * This param is useful when tracking single-page landing websites.\n     */\n    unique?: boolean;\n    /** Send Heartbeat requests when the website tab is not active in the browser. */\n    heartbeatOnBackground?: boolean;\n    /**\n     * Set to `true` to enable hash-based routing.\n     * For example if you have pages like /#/path or want to track pages like /path#hash\n     */\n    hash?: boolean;\n    /**\n     * Set to `true` to enable search-based routing.\n     * For example if you have pages like /path?search\n     */\n    search?: boolean;\n    /**\n     * Callback to edit / prevent sending pageviews.\n     *\n     * @param payload - The pageview payload.\n     * @returns The edited payload or `false` to prevent sending the pageview. If `true` is returned, the payload will be sent as-is.\n     */\n    callback?: (payload: IPageViewPayload) => Partial<IPageViewPayload> | boolean;\n}\nexport declare const defaultActions: {\n    stop(): void;\n};\nexport declare class Lib {\n    private projectID;\n    private options?;\n    private pageData;\n    private pageViewsOptions?;\n    private errorsOptions?;\n    private perfStatsCollected;\n    private activePage;\n    private errorListenerExists;\n    private cachedData;\n    constructor(projectID: string, options?: LibOptions | undefined);\n    captureError(event: ErrorEvent): void;\n    trackErrors(options?: ErrorOptions): ErrorActions;\n    submitError(payload: IErrorEventPayload, evokeCallback?: boolean): void;\n    track(event: TrackEventOptions): Promise<void>;\n    trackPageViews(options?: PageViewsOptions): PageActions;\n    getPerformanceStats(): IPerfPayload | {};\n    /**\n     * Fetches all feature flags and experiments for the project.\n     * Results are cached for 5 minutes by default.\n     *\n     * @param options - Options for evaluating feature flags.\n     * @param forceRefresh - If true, bypasses the cache and fetches fresh data.\n     * @returns A promise that resolves to a record of flag keys to boolean values.\n     */\n    getFeatureFlags(options?: FeatureFlagsOptions, forceRefresh?: boolean): Promise<Record<string, boolean>>;\n    /**\n     * Internal method to fetch both feature flags and experiments from the API.\n     */\n    private fetchFlagsAndExperiments;\n    /**\n     * Gets the value of a single feature flag.\n     *\n     * @param key - The feature flag key.\n     * @param options - Options for evaluating the feature flag.\n     * @param defaultValue - Default value to return if the flag is not found. Defaults to false.\n     * @returns A promise that resolves to the boolean value of the flag.\n     */\n    getFeatureFlag(key: string, options?: FeatureFlagsOptions, defaultValue?: boolean): Promise<boolean>;\n    /**\n     * Clears the cached feature flags and experiments, forcing a fresh fetch on the next call.\n     */\n    clearFeatureFlagsCache(): void;\n    /**\n     * Fetches all A/B test experiments for the project.\n     * Results are cached for 5 minutes by default (shared cache with feature flags).\n     *\n     * @param options - Options for evaluating experiments.\n     * @param forceRefresh - If true, bypasses the cache and fetches fresh data.\n     * @returns A promise that resolves to a record of experiment IDs to variant keys.\n     *\n     * @example\n     * ```typescript\n     * const experiments = await getExperiments()\n     * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }\n     * ```\n     */\n    getExperiments(options?: ExperimentOptions, forceRefresh?: boolean): Promise<Record<string, string>>;\n    /**\n     * Gets the variant key for a specific A/B test experiment.\n     *\n     * @param experimentId - The experiment ID.\n     * @param options - Options for evaluating the experiment.\n     * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.\n     * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.\n     *\n     * @example\n     * ```typescript\n     * const variant = await getExperiment('checkout-redesign')\n     *\n     * if (variant === 'new-checkout') {\n     *   // Show new checkout flow\n     * } else {\n     *   // Show control (original) checkout\n     * }\n     * ```\n     */\n    getExperiment(experimentId: string, options?: ExperimentOptions, defaultVariant?: string | null): Promise<string | null>;\n    /**\n     * Clears the cached experiments (alias for clearFeatureFlagsCache since they share the same cache).\n     */\n    clearExperimentsCache(): void;\n    /**\n     * Gets the anonymous profile ID for the current visitor.\n     * If profileId was set via init options, returns that.\n     * Otherwise, requests server to generate one from IP/UA hash.\n     *\n     * This ID can be used for revenue attribution with payment providers.\n     *\n     * @returns A promise that resolves to the profile ID string, or null on error.\n     *\n     * @example\n     * ```typescript\n     * const profileId = await swetrix.getProfileId()\n     *\n     * // Pass to Paddle Checkout for revenue attribution\n     * Paddle.Checkout.open({\n     *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n     *   customData: {\n     *     swetrix_profile_id: profileId,\n     *     swetrix_session_id: await swetrix.getSessionId()\n     *   }\n     * })\n     * ```\n     */\n    getProfileId(): Promise<string | null>;\n    /**\n     * Gets the current session ID for the visitor.\n     * Session IDs are generated server-side based on IP and user agent.\n     *\n     * This ID can be used for revenue attribution with payment providers.\n     *\n     * @returns A promise that resolves to the session ID string, or null on error.\n     *\n     * @example\n     * ```typescript\n     * const sessionId = await swetrix.getSessionId()\n     *\n     * // Pass to Paddle Checkout for revenue attribution\n     * Paddle.Checkout.open({\n     *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n     *   customData: {\n     *     swetrix_profile_id: await swetrix.getProfileId(),\n     *     swetrix_session_id: sessionId\n     *   }\n     * })\n     * ```\n     */\n    getSessionId(): Promise<string | null>;\n    /**\n     * Gets the API base URL (without /log suffix).\n     */\n    private getApiBase;\n    private heartbeat;\n    private trackPathChange;\n    private trackPage;\n    submitPageView(payload: Partial<IPageViewPayload>, unique: boolean, perf: IPerfPayload | {}, evokeCallback?: boolean): void;\n    private canTrack;\n    private sendRequest;\n}\nexport {};\n"
  },
  {
    "path": "dist/esnext/Lib.js",
    "content": "import { isInBrowser, isLocalhost, isAutomated, getLocale, getTimezone, getReferrer, getUTMCampaign, getUTMMedium, getUTMSource, getUTMTerm, getUTMContent, getPath, } from './utils.js';\nexport const defaultActions = {\n    stop() { },\n};\nconst DEFAULT_API_HOST = 'https://api.swetrix.com/log';\nconst DEFAULT_API_BASE = 'https://api.swetrix.com';\n// Default cache duration: 5 minutes\nconst DEFAULT_CACHE_DURATION = 5 * 60 * 1000;\nexport class Lib {\n    constructor(projectID, options) {\n        this.projectID = projectID;\n        this.options = options;\n        this.pageData = null;\n        this.pageViewsOptions = null;\n        this.errorsOptions = null;\n        this.perfStatsCollected = false;\n        this.activePage = null;\n        this.errorListenerExists = false;\n        this.cachedData = null;\n        this.trackPathChange = this.trackPathChange.bind(this);\n        this.heartbeat = this.heartbeat.bind(this);\n        this.captureError = this.captureError.bind(this);\n    }\n    captureError(event) {\n        if (typeof this.errorsOptions?.sampleRate === 'number' && this.errorsOptions.sampleRate >= Math.random()) {\n            return;\n        }\n        this.submitError({\n            // The file in which error occured.\n            filename: event.filename,\n            // The line of code error occured on.\n            lineno: event.lineno,\n            // The column of code error occured on.\n            colno: event.colno,\n            // Name of the error, if not exists (i.e. it's a custom thrown error). The initial value of name is \"Error\", but just in case lets explicitly set it here too.\n            name: event.error?.name || 'Error',\n            // Description of the error. By default, we use message from Error object, is it does not contain the error name\n            // (we want to split error name and message so we could group them together later in dashboard).\n            // If message in error object does not exist - lets use a message from the Error event itself.\n            message: event.error?.message || event.message,\n            // Stack trace of the error, if available.\n            stackTrace: event.error?.stack,\n        }, true);\n    }\n    trackErrors(options) {\n        if (this.errorListenerExists || !this.canTrack()) {\n            return defaultActions;\n        }\n        this.errorsOptions = options;\n        window.addEventListener('error', this.captureError);\n        this.errorListenerExists = true;\n        return {\n            stop: () => {\n                window.removeEventListener('error', this.captureError);\n                this.errorListenerExists = false;\n            },\n        };\n    }\n    submitError(payload, evokeCallback) {\n        const privateData = {\n            pid: this.projectID,\n        };\n        const errorPayload = {\n            pg: this.activePage ||\n                getPath({\n                    hash: this.pageViewsOptions?.hash,\n                    search: this.pageViewsOptions?.search,\n                }),\n            lc: getLocale(),\n            tz: getTimezone(),\n            ...payload,\n        };\n        if (evokeCallback && this.errorsOptions?.callback) {\n            const callbackResult = this.errorsOptions.callback(errorPayload);\n            if (callbackResult === false) {\n                return;\n            }\n            if (callbackResult && typeof callbackResult === 'object') {\n                Object.assign(errorPayload, callbackResult);\n            }\n        }\n        Object.assign(errorPayload, privateData);\n        this.sendRequest('error', errorPayload);\n    }\n    async track(event) {\n        if (!this.canTrack()) {\n            return;\n        }\n        const data = {\n            ...event,\n            pid: this.projectID,\n            pg: this.activePage ||\n                getPath({\n                    hash: this.pageViewsOptions?.hash,\n                    search: this.pageViewsOptions?.search,\n                }),\n            lc: getLocale(),\n            tz: getTimezone(),\n            ref: getReferrer(),\n            so: getUTMSource(),\n            me: getUTMMedium(),\n            ca: getUTMCampaign(),\n            te: getUTMTerm(),\n            co: getUTMContent(),\n            profileId: event.profileId ?? this.options?.profileId,\n        };\n        await this.sendRequest('custom', data);\n    }\n    trackPageViews(options) {\n        if (!this.canTrack()) {\n            return defaultActions;\n        }\n        if (this.pageData) {\n            return this.pageData.actions;\n        }\n        this.pageViewsOptions = options;\n        let interval;\n        if (!options?.unique) {\n            interval = setInterval(this.trackPathChange, 2000);\n        }\n        setTimeout(this.heartbeat, 3000);\n        const hbInterval = setInterval(this.heartbeat, 28000);\n        const path = getPath({\n            hash: options?.hash,\n            search: options?.search,\n        });\n        this.pageData = {\n            path,\n            actions: {\n                stop: () => {\n                    clearInterval(interval);\n                    clearInterval(hbInterval);\n                },\n            },\n        };\n        this.trackPage(path, options?.unique);\n        return this.pageData.actions;\n    }\n    getPerformanceStats() {\n        if (!this.canTrack() || this.perfStatsCollected || !window.performance?.getEntriesByType) {\n            return {};\n        }\n        const perf = window.performance.getEntriesByType('navigation')[0];\n        if (!perf) {\n            return {};\n        }\n        this.perfStatsCollected = true;\n        return {\n            // Network\n            dns: perf.domainLookupEnd - perf.domainLookupStart, // DNS Resolution\n            tls: perf.secureConnectionStart ? perf.requestStart - perf.secureConnectionStart : 0, // TLS Setup; checking if secureConnectionStart is not 0 (it's 0 for non-https websites)\n            conn: perf.secureConnectionStart\n                ? perf.secureConnectionStart - perf.connectStart\n                : perf.connectEnd - perf.connectStart, // Connection time\n            response: perf.responseEnd - perf.responseStart, // Response Time (Download)\n            // Frontend\n            render: perf.domComplete - perf.domContentLoadedEventEnd, // Browser rendering the HTML time\n            dom_load: perf.domContentLoadedEventEnd - perf.responseEnd, // DOM loading timing\n            page_load: perf.loadEventStart, // Page load time\n            // Backend\n            ttfb: perf.responseStart - perf.requestStart,\n        };\n    }\n    /**\n     * Fetches all feature flags and experiments for the project.\n     * Results are cached for 5 minutes by default.\n     *\n     * @param options - Options for evaluating feature flags.\n     * @param forceRefresh - If true, bypasses the cache and fetches fresh data.\n     * @returns A promise that resolves to a record of flag keys to boolean values.\n     */\n    async getFeatureFlags(options, forceRefresh) {\n        if (!isInBrowser()) {\n            return {};\n        }\n        const requestedProfileId = options?.profileId ?? this.options?.profileId;\n        // Check cache first - must match profileId and not be expired\n        if (!forceRefresh && this.cachedData) {\n            const now = Date.now();\n            const isSameProfile = this.cachedData.profileId === requestedProfileId;\n            if (isSameProfile && now - this.cachedData.timestamp < DEFAULT_CACHE_DURATION) {\n                return this.cachedData.flags;\n            }\n        }\n        try {\n            await this.fetchFlagsAndExperiments(options);\n            return this.cachedData?.flags || {};\n        }\n        catch (error) {\n            console.warn('[Swetrix] Error fetching feature flags:', error);\n            return this.cachedData?.flags || {};\n        }\n    }\n    /**\n     * Internal method to fetch both feature flags and experiments from the API.\n     */\n    async fetchFlagsAndExperiments(options) {\n        const apiBase = this.getApiBase();\n        const body = {\n            pid: this.projectID,\n        };\n        // Use profileId from options, or fall back to global profileId\n        const profileId = options?.profileId ?? this.options?.profileId;\n        if (profileId) {\n            body.profileId = profileId;\n        }\n        const response = await fetch(`${apiBase}/feature-flag/evaluate`, {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json',\n            },\n            body: JSON.stringify(body),\n        });\n        if (!response.ok) {\n            console.warn('[Swetrix] Failed to fetch feature flags and experiments:', response.status);\n            return;\n        }\n        const data = (await response.json());\n        // Use profileId from options, or fall back to global profileId\n        const cachedProfileId = options?.profileId ?? this.options?.profileId;\n        // Update cache with both flags and experiments\n        this.cachedData = {\n            flags: data.flags || {},\n            experiments: data.experiments || {},\n            timestamp: Date.now(),\n            profileId: cachedProfileId,\n        };\n    }\n    /**\n     * Gets the value of a single feature flag.\n     *\n     * @param key - The feature flag key.\n     * @param options - Options for evaluating the feature flag.\n     * @param defaultValue - Default value to return if the flag is not found. Defaults to false.\n     * @returns A promise that resolves to the boolean value of the flag.\n     */\n    async getFeatureFlag(key, options, defaultValue = false) {\n        const flags = await this.getFeatureFlags(options);\n        return flags[key] ?? defaultValue;\n    }\n    /**\n     * Clears the cached feature flags and experiments, forcing a fresh fetch on the next call.\n     */\n    clearFeatureFlagsCache() {\n        this.cachedData = null;\n    }\n    /**\n     * Fetches all A/B test experiments for the project.\n     * Results are cached for 5 minutes by default (shared cache with feature flags).\n     *\n     * @param options - Options for evaluating experiments.\n     * @param forceRefresh - If true, bypasses the cache and fetches fresh data.\n     * @returns A promise that resolves to a record of experiment IDs to variant keys.\n     *\n     * @example\n     * ```typescript\n     * const experiments = await getExperiments()\n     * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }\n     * ```\n     */\n    async getExperiments(options, forceRefresh) {\n        if (!isInBrowser()) {\n            return {};\n        }\n        const requestedProfileId = options?.profileId ?? this.options?.profileId;\n        // Check cache first - must match profileId and not be expired\n        if (!forceRefresh && this.cachedData) {\n            const now = Date.now();\n            const isSameProfile = this.cachedData.profileId === requestedProfileId;\n            if (isSameProfile && now - this.cachedData.timestamp < DEFAULT_CACHE_DURATION) {\n                return this.cachedData.experiments;\n            }\n        }\n        try {\n            await this.fetchFlagsAndExperiments(options);\n            return this.cachedData?.experiments || {};\n        }\n        catch (error) {\n            console.warn('[Swetrix] Error fetching experiments:', error);\n            return this.cachedData?.experiments || {};\n        }\n    }\n    /**\n     * Gets the variant key for a specific A/B test experiment.\n     *\n     * @param experimentId - The experiment ID.\n     * @param options - Options for evaluating the experiment.\n     * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.\n     * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.\n     *\n     * @example\n     * ```typescript\n     * const variant = await getExperiment('checkout-redesign')\n     *\n     * if (variant === 'new-checkout') {\n     *   // Show new checkout flow\n     * } else {\n     *   // Show control (original) checkout\n     * }\n     * ```\n     */\n    async getExperiment(experimentId, options, defaultVariant = null) {\n        const experiments = await this.getExperiments(options);\n        return experiments[experimentId] ?? defaultVariant;\n    }\n    /**\n     * Clears the cached experiments (alias for clearFeatureFlagsCache since they share the same cache).\n     */\n    clearExperimentsCache() {\n        this.cachedData = null;\n    }\n    /**\n     * Gets the anonymous profile ID for the current visitor.\n     * If profileId was set via init options, returns that.\n     * Otherwise, requests server to generate one from IP/UA hash.\n     *\n     * This ID can be used for revenue attribution with payment providers.\n     *\n     * @returns A promise that resolves to the profile ID string, or null on error.\n     *\n     * @example\n     * ```typescript\n     * const profileId = await swetrix.getProfileId()\n     *\n     * // Pass to Paddle Checkout for revenue attribution\n     * Paddle.Checkout.open({\n     *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n     *   customData: {\n     *     swetrix_profile_id: profileId,\n     *     swetrix_session_id: await swetrix.getSessionId()\n     *   }\n     * })\n     * ```\n     */\n    async getProfileId() {\n        // If profileId is already set in options, return it\n        if (this.options?.profileId) {\n            return this.options.profileId;\n        }\n        if (!isInBrowser()) {\n            return null;\n        }\n        try {\n            const apiBase = this.getApiBase();\n            const response = await fetch(`${apiBase}/log/profile-id`, {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json',\n                },\n                body: JSON.stringify({ pid: this.projectID }),\n            });\n            if (!response.ok) {\n                return null;\n            }\n            const data = (await response.json());\n            return data.profileId;\n        }\n        catch {\n            return null;\n        }\n    }\n    /**\n     * Gets the current session ID for the visitor.\n     * Session IDs are generated server-side based on IP and user agent.\n     *\n     * This ID can be used for revenue attribution with payment providers.\n     *\n     * @returns A promise that resolves to the session ID string, or null on error.\n     *\n     * @example\n     * ```typescript\n     * const sessionId = await swetrix.getSessionId()\n     *\n     * // Pass to Paddle Checkout for revenue attribution\n     * Paddle.Checkout.open({\n     *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n     *   customData: {\n     *     swetrix_profile_id: await swetrix.getProfileId(),\n     *     swetrix_session_id: sessionId\n     *   }\n     * })\n     * ```\n     */\n    async getSessionId() {\n        if (!isInBrowser()) {\n            return null;\n        }\n        try {\n            const apiBase = this.getApiBase();\n            const response = await fetch(`${apiBase}/log/session-id`, {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json',\n                },\n                body: JSON.stringify({ pid: this.projectID }),\n            });\n            if (!response.ok) {\n                return null;\n            }\n            const data = (await response.json());\n            return data.sessionId;\n        }\n        catch {\n            return null;\n        }\n    }\n    /**\n     * Gets the API base URL (without /log suffix).\n     */\n    getApiBase() {\n        if (this.options?.apiURL) {\n            // Remove trailing /log if present\n            return this.options.apiURL.replace(/\\/log\\/?$/, '');\n        }\n        return DEFAULT_API_BASE;\n    }\n    heartbeat() {\n        if (!this.pageViewsOptions?.heartbeatOnBackground && document.visibilityState === 'hidden') {\n            return;\n        }\n        const data = {\n            pid: this.projectID,\n        };\n        if (this.options?.profileId) {\n            data.profileId = this.options.profileId;\n        }\n        this.sendRequest('hb', data);\n    }\n    // Tracking path changes. If path changes -> calling this.trackPage method\n    trackPathChange() {\n        if (!this.pageData)\n            return;\n        const newPath = getPath({\n            hash: this.pageViewsOptions?.hash,\n            search: this.pageViewsOptions?.search,\n        });\n        const { path } = this.pageData;\n        if (path !== newPath) {\n            this.trackPage(newPath, false);\n        }\n    }\n    trackPage(pg, unique = false) {\n        if (!this.pageData)\n            return;\n        this.pageData.path = pg;\n        const perf = this.getPerformanceStats();\n        this.activePage = pg;\n        this.submitPageView({ pg }, unique, perf, true);\n    }\n    submitPageView(payload, unique, perf, evokeCallback) {\n        const privateData = {\n            pid: this.projectID,\n            perf,\n            unique,\n        };\n        const pvPayload = {\n            lc: getLocale(),\n            tz: getTimezone(),\n            ref: getReferrer(),\n            so: getUTMSource(),\n            me: getUTMMedium(),\n            ca: getUTMCampaign(),\n            te: getUTMTerm(),\n            co: getUTMContent(),\n            profileId: this.options?.profileId,\n            ...payload,\n        };\n        if (evokeCallback && this.pageViewsOptions?.callback) {\n            const callbackResult = this.pageViewsOptions.callback(pvPayload);\n            if (callbackResult === false) {\n                return;\n            }\n            if (callbackResult && typeof callbackResult === 'object') {\n                Object.assign(pvPayload, callbackResult);\n            }\n        }\n        Object.assign(pvPayload, privateData);\n        this.sendRequest('', pvPayload);\n    }\n    canTrack() {\n        if (this.options?.disabled ||\n            !isInBrowser() ||\n            (this.options?.respectDNT && window.navigator?.doNotTrack === '1') ||\n            (!this.options?.devMode && isLocalhost()) ||\n            isAutomated()) {\n            return false;\n        }\n        return true;\n    }\n    async sendRequest(path, body) {\n        const host = this.options?.apiURL || DEFAULT_API_HOST;\n        await fetch(`${host}/${path}`, {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json',\n            },\n            body: JSON.stringify(body),\n        });\n    }\n}\n//# sourceMappingURL=Lib.js.map"
  },
  {
    "path": "dist/esnext/index.d.ts",
    "content": "import { Lib, LibOptions, TrackEventOptions, PageViewsOptions, ErrorOptions, PageActions, ErrorActions, IErrorEventPayload, IPageViewPayload, FeatureFlagsOptions, ExperimentOptions } from './Lib.js';\nexport declare let LIB_INSTANCE: Lib | null;\n/**\n * Initialise the tracking library instance (other methods won't work if the library is not initialised).\n *\n * @param {string} pid The Project ID to link the instance of Swetrix.js to.\n * @param {LibOptions} options Options related to the tracking.\n * @returns {Lib} Instance of the Swetrix.js.\n */\nexport declare function init(pid: string, options?: LibOptions): Lib;\n/**\n * With this function you are able to track any custom events you want.\n * You should never send any identifiable data (like User ID, email, session cookie, etc.) as an event name.\n * The total number of track calls and their conversion rate will be saved.\n *\n * @param {TrackEventOptions} event The options related to the custom event.\n */\nexport declare function track(event: TrackEventOptions): Promise<void>;\n/**\n * With this function you are able to automatically track pageviews across your application.\n *\n * @param {PageViewsOptions} options Pageviews tracking options.\n * @returns {PageActions} The actions related to the tracking. Used to stop tracking pages.\n */\nexport declare function trackViews(options?: PageViewsOptions): Promise<PageActions>;\n/**\n * This function is used to set up automatic error events tracking.\n * It set's up an error listener, and whenever an error happens, it gets tracked.\n *\n * @returns {ErrorActions} The actions related to the tracking. Used to stop tracking errors.\n */\nexport declare function trackErrors(options?: ErrorOptions): ErrorActions;\n/**\n * This function is used to manually track an error event.\n * It's useful if you want to track specific errors in your application.\n *\n * @param payload Swetrix error object to send.\n * @returns void\n */\nexport declare function trackError(payload: IErrorEventPayload): void;\n/**\n * This function is used to manually track a page view event.\n * It's useful if your application uses esoteric routing which is not supported by Swetrix by default.\n *\n * @deprecated This function is deprecated and will be removed soon, please use the `pageview` instead.\n * @param pg Path of the page to track (this will be sent to the Swetrix API and displayed in the dashboard).\n * @param _prev Path of the previous page (deprecated and ignored).\n * @param unique If set to `true`, only 1 event with the same ID will be saved per user session.\n * @returns void\n */\nexport declare function trackPageview(pg: string, _prev?: string, unique?: boolean): void;\nexport interface IPageviewOptions {\n    payload: Partial<IPageViewPayload>;\n    unique?: boolean;\n}\nexport declare function pageview(options: IPageviewOptions): void;\n/**\n * Fetches all feature flags for the project.\n * Results are cached for 5 minutes by default.\n *\n * @param options - Options for evaluating feature flags (visitorId, attributes).\n * @param forceRefresh - If true, bypasses the cache and fetches fresh flags.\n * @returns A promise that resolves to a record of flag keys to boolean values.\n *\n * @example\n * ```typescript\n * const flags = await getFeatureFlags({\n *   visitorId: 'user-123',\n *   attributes: { cc: 'US', dv: 'desktop' }\n * })\n *\n * if (flags['new-checkout']) {\n *   // Show new checkout flow\n * }\n * ```\n */\nexport declare function getFeatureFlags(options?: FeatureFlagsOptions, forceRefresh?: boolean): Promise<Record<string, boolean>>;\n/**\n * Gets the value of a single feature flag.\n *\n * @param key - The feature flag key.\n * @param options - Options for evaluating the feature flag (visitorId, attributes).\n * @param defaultValue - Default value to return if the flag is not found. Defaults to false.\n * @returns A promise that resolves to the boolean value of the flag.\n *\n * @example\n * ```typescript\n * const isEnabled = await getFeatureFlag('dark-mode', { visitorId: 'user-123' })\n *\n * if (isEnabled) {\n *   // Enable dark mode\n * }\n * ```\n */\nexport declare function getFeatureFlag(key: string, options?: FeatureFlagsOptions, defaultValue?: boolean): Promise<boolean>;\n/**\n * Clears the cached feature flags, forcing a fresh fetch on the next call.\n * Useful when you know the user's context has changed significantly.\n */\nexport declare function clearFeatureFlagsCache(): void;\n/**\n * Fetches all A/B test experiments for the project.\n * Results are cached for 5 minutes by default (shared cache with feature flags).\n *\n * @param options - Options for evaluating experiments.\n * @param forceRefresh - If true, bypasses the cache and fetches fresh data.\n * @returns A promise that resolves to a record of experiment IDs to variant keys.\n *\n * @example\n * ```typescript\n * const experiments = await getExperiments()\n * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }\n *\n * // Use the assigned variant\n * const checkoutVariant = experiments['checkout-experiment-id']\n * if (checkoutVariant === 'new-checkout') {\n *   showNewCheckout()\n * } else {\n *   showOriginalCheckout()\n * }\n * ```\n */\nexport declare function getExperiments(options?: ExperimentOptions, forceRefresh?: boolean): Promise<Record<string, string>>;\n/**\n * Gets the variant key for a specific A/B test experiment.\n *\n * @param experimentId - The experiment ID.\n * @param options - Options for evaluating the experiment.\n * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.\n * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.\n *\n * @example\n * ```typescript\n * const variant = await getExperiment('checkout-redesign-experiment-id')\n *\n * if (variant === 'new-checkout') {\n *   // Show new checkout flow\n *   showNewCheckout()\n * } else if (variant === 'control') {\n *   // Show original checkout (control group)\n *   showOriginalCheckout()\n * } else {\n *   // Experiment not running or user not included\n *   showOriginalCheckout()\n * }\n * ```\n */\nexport declare function getExperiment(experimentId: string, options?: ExperimentOptions, defaultVariant?: string | null): Promise<string | null>;\n/**\n * Clears the cached experiments, forcing a fresh fetch on the next call.\n * This is an alias for clearFeatureFlagsCache since experiments and flags share the same cache.\n */\nexport declare function clearExperimentsCache(): void;\n/**\n * Gets the anonymous profile ID for the current visitor.\n * If profileId was set via init options, returns that.\n * Otherwise, requests server to generate one from IP/UA hash.\n *\n * This ID can be used for revenue attribution with payment providers like Paddle.\n *\n * @returns A promise that resolves to the profile ID string, or null on error.\n *\n * @example\n * ```typescript\n * const profileId = await getProfileId()\n *\n * // Pass to Paddle Checkout for revenue attribution\n * Paddle.Checkout.open({\n *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n *   customData: {\n *     swetrix_profile_id: profileId,\n *     swetrix_session_id: await getSessionId()\n *   }\n * })\n * ```\n */\nexport declare function getProfileId(): Promise<string | null>;\n/**\n * Gets the current session ID for the visitor.\n * Session IDs are generated server-side based on IP and user agent.\n *\n * This ID can be used for revenue attribution with payment providers like Paddle.\n *\n * @returns A promise that resolves to the session ID string, or null on error.\n *\n * @example\n * ```typescript\n * const sessionId = await getSessionId()\n *\n * // Pass to Paddle Checkout for revenue attribution\n * Paddle.Checkout.open({\n *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n *   customData: {\n *     swetrix_profile_id: await getProfileId(),\n *     swetrix_session_id: sessionId\n *   }\n * })\n * ```\n */\nexport declare function getSessionId(): Promise<string | null>;\nexport { LibOptions, TrackEventOptions, PageViewsOptions, ErrorOptions, PageActions, ErrorActions, IErrorEventPayload, IPageViewPayload, FeatureFlagsOptions, ExperimentOptions, };\n"
  },
  {
    "path": "dist/esnext/index.js",
    "content": "import { Lib, defaultActions, } from './Lib.js';\nexport let LIB_INSTANCE = null;\n/**\n * Initialise the tracking library instance (other methods won't work if the library is not initialised).\n *\n * @param {string} pid The Project ID to link the instance of Swetrix.js to.\n * @param {LibOptions} options Options related to the tracking.\n * @returns {Lib} Instance of the Swetrix.js.\n */\nexport function init(pid, options) {\n    if (!LIB_INSTANCE) {\n        LIB_INSTANCE = new Lib(pid, options);\n    }\n    return LIB_INSTANCE;\n}\n/**\n * With this function you are able to track any custom events you want.\n * You should never send any identifiable data (like User ID, email, session cookie, etc.) as an event name.\n * The total number of track calls and their conversion rate will be saved.\n *\n * @param {TrackEventOptions} event The options related to the custom event.\n */\nexport async function track(event) {\n    if (!LIB_INSTANCE)\n        return;\n    await LIB_INSTANCE.track(event);\n}\n/**\n * With this function you are able to automatically track pageviews across your application.\n *\n * @param {PageViewsOptions} options Pageviews tracking options.\n * @returns {PageActions} The actions related to the tracking. Used to stop tracking pages.\n */\nexport function trackViews(options) {\n    return new Promise((resolve) => {\n        if (!LIB_INSTANCE) {\n            resolve(defaultActions);\n            return;\n        }\n        // We need to verify that document.readyState is complete for the performance stats to be collected correctly.\n        if (typeof document === 'undefined' || document.readyState === 'complete') {\n            resolve(LIB_INSTANCE.trackPageViews(options));\n        }\n        else {\n            window.addEventListener('load', () => {\n                // @ts-ignore\n                resolve(LIB_INSTANCE.trackPageViews(options));\n            });\n        }\n    });\n}\n/**\n * This function is used to set up automatic error events tracking.\n * It set's up an error listener, and whenever an error happens, it gets tracked.\n *\n * @returns {ErrorActions} The actions related to the tracking. Used to stop tracking errors.\n */\nexport function trackErrors(options) {\n    if (!LIB_INSTANCE) {\n        return defaultActions;\n    }\n    return LIB_INSTANCE.trackErrors(options);\n}\n/**\n * This function is used to manually track an error event.\n * It's useful if you want to track specific errors in your application.\n *\n * @param payload Swetrix error object to send.\n * @returns void\n */\nexport function trackError(payload) {\n    if (!LIB_INSTANCE)\n        return;\n    LIB_INSTANCE.submitError(payload, false);\n}\n/**\n * This function is used to manually track a page view event.\n * It's useful if your application uses esoteric routing which is not supported by Swetrix by default.\n *\n * @deprecated This function is deprecated and will be removed soon, please use the `pageview` instead.\n * @param pg Path of the page to track (this will be sent to the Swetrix API and displayed in the dashboard).\n * @param _prev Path of the previous page (deprecated and ignored).\n * @param unique If set to `true`, only 1 event with the same ID will be saved per user session.\n * @returns void\n */\nexport function trackPageview(pg, _prev, unique) {\n    if (!LIB_INSTANCE)\n        return;\n    LIB_INSTANCE.submitPageView({ pg }, Boolean(unique), {});\n}\nexport function pageview(options) {\n    if (!LIB_INSTANCE)\n        return;\n    LIB_INSTANCE.submitPageView(options.payload, Boolean(options.unique), {});\n}\n/**\n * Fetches all feature flags for the project.\n * Results are cached for 5 minutes by default.\n *\n * @param options - Options for evaluating feature flags (visitorId, attributes).\n * @param forceRefresh - If true, bypasses the cache and fetches fresh flags.\n * @returns A promise that resolves to a record of flag keys to boolean values.\n *\n * @example\n * ```typescript\n * const flags = await getFeatureFlags({\n *   visitorId: 'user-123',\n *   attributes: { cc: 'US', dv: 'desktop' }\n * })\n *\n * if (flags['new-checkout']) {\n *   // Show new checkout flow\n * }\n * ```\n */\nexport async function getFeatureFlags(options, forceRefresh) {\n    if (!LIB_INSTANCE)\n        return {};\n    return LIB_INSTANCE.getFeatureFlags(options, forceRefresh);\n}\n/**\n * Gets the value of a single feature flag.\n *\n * @param key - The feature flag key.\n * @param options - Options for evaluating the feature flag (visitorId, attributes).\n * @param defaultValue - Default value to return if the flag is not found. Defaults to false.\n * @returns A promise that resolves to the boolean value of the flag.\n *\n * @example\n * ```typescript\n * const isEnabled = await getFeatureFlag('dark-mode', { visitorId: 'user-123' })\n *\n * if (isEnabled) {\n *   // Enable dark mode\n * }\n * ```\n */\nexport async function getFeatureFlag(key, options, defaultValue = false) {\n    if (!LIB_INSTANCE)\n        return defaultValue;\n    return LIB_INSTANCE.getFeatureFlag(key, options, defaultValue);\n}\n/**\n * Clears the cached feature flags, forcing a fresh fetch on the next call.\n * Useful when you know the user's context has changed significantly.\n */\nexport function clearFeatureFlagsCache() {\n    if (!LIB_INSTANCE)\n        return;\n    LIB_INSTANCE.clearFeatureFlagsCache();\n}\n/**\n * Fetches all A/B test experiments for the project.\n * Results are cached for 5 minutes by default (shared cache with feature flags).\n *\n * @param options - Options for evaluating experiments.\n * @param forceRefresh - If true, bypasses the cache and fetches fresh data.\n * @returns A promise that resolves to a record of experiment IDs to variant keys.\n *\n * @example\n * ```typescript\n * const experiments = await getExperiments()\n * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }\n *\n * // Use the assigned variant\n * const checkoutVariant = experiments['checkout-experiment-id']\n * if (checkoutVariant === 'new-checkout') {\n *   showNewCheckout()\n * } else {\n *   showOriginalCheckout()\n * }\n * ```\n */\nexport async function getExperiments(options, forceRefresh) {\n    if (!LIB_INSTANCE)\n        return {};\n    return LIB_INSTANCE.getExperiments(options, forceRefresh);\n}\n/**\n * Gets the variant key for a specific A/B test experiment.\n *\n * @param experimentId - The experiment ID.\n * @param options - Options for evaluating the experiment.\n * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.\n * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.\n *\n * @example\n * ```typescript\n * const variant = await getExperiment('checkout-redesign-experiment-id')\n *\n * if (variant === 'new-checkout') {\n *   // Show new checkout flow\n *   showNewCheckout()\n * } else if (variant === 'control') {\n *   // Show original checkout (control group)\n *   showOriginalCheckout()\n * } else {\n *   // Experiment not running or user not included\n *   showOriginalCheckout()\n * }\n * ```\n */\nexport async function getExperiment(experimentId, options, defaultVariant = null) {\n    if (!LIB_INSTANCE)\n        return defaultVariant;\n    return LIB_INSTANCE.getExperiment(experimentId, options, defaultVariant);\n}\n/**\n * Clears the cached experiments, forcing a fresh fetch on the next call.\n * This is an alias for clearFeatureFlagsCache since experiments and flags share the same cache.\n */\nexport function clearExperimentsCache() {\n    if (!LIB_INSTANCE)\n        return;\n    LIB_INSTANCE.clearExperimentsCache();\n}\n/**\n * Gets the anonymous profile ID for the current visitor.\n * If profileId was set via init options, returns that.\n * Otherwise, requests server to generate one from IP/UA hash.\n *\n * This ID can be used for revenue attribution with payment providers like Paddle.\n *\n * @returns A promise that resolves to the profile ID string, or null on error.\n *\n * @example\n * ```typescript\n * const profileId = await getProfileId()\n *\n * // Pass to Paddle Checkout for revenue attribution\n * Paddle.Checkout.open({\n *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n *   customData: {\n *     swetrix_profile_id: profileId,\n *     swetrix_session_id: await getSessionId()\n *   }\n * })\n * ```\n */\nexport async function getProfileId() {\n    if (!LIB_INSTANCE)\n        return null;\n    return LIB_INSTANCE.getProfileId();\n}\n/**\n * Gets the current session ID for the visitor.\n * Session IDs are generated server-side based on IP and user agent.\n *\n * This ID can be used for revenue attribution with payment providers like Paddle.\n *\n * @returns A promise that resolves to the session ID string, or null on error.\n *\n * @example\n * ```typescript\n * const sessionId = await getSessionId()\n *\n * // Pass to Paddle Checkout for revenue attribution\n * Paddle.Checkout.open({\n *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n *   customData: {\n *     swetrix_profile_id: await getProfileId(),\n *     swetrix_session_id: sessionId\n *   }\n * })\n * ```\n */\nexport async function getSessionId() {\n    if (!LIB_INSTANCE)\n        return null;\n    return LIB_INSTANCE.getSessionId();\n}\n//# sourceMappingURL=index.js.map"
  },
  {
    "path": "dist/esnext/utils.d.ts",
    "content": "interface IGetPath {\n    hash?: boolean;\n    search?: boolean;\n}\nexport declare const isInBrowser: () => boolean;\nexport declare const isLocalhost: () => boolean;\nexport declare const isAutomated: () => boolean;\nexport declare const getLocale: () => string;\nexport declare const getTimezone: () => string | undefined;\nexport declare const getReferrer: () => string | undefined;\nexport declare const getUTMSource: () => string | undefined;\nexport declare const getUTMMedium: () => string | undefined;\nexport declare const getUTMCampaign: () => string | undefined;\nexport declare const getUTMTerm: () => string | undefined;\nexport declare const getUTMContent: () => string | undefined;\n/**\n * Function used to track the current page (path) of the application.\n * Will work in cases where the path looks like:\n * - /path\n * - /#/path\n * - /path?search\n * - /path?search#hash\n * - /path#hash?search\n *\n * @param options - Options for the function.\n * @param options.hash - Whether to trigger on hash change.\n * @param options.search - Whether to trigger on search change.\n * @returns The path of the current page.\n */\nexport declare const getPath: (options: IGetPath) => string;\nexport {};\n"
  },
  {
    "path": "dist/esnext/utils.js",
    "content": "const findInSearch = (exp) => {\n    const res = location.search.match(exp);\n    return (res && res[2]) || undefined;\n};\nconst utmSourceRegex = /[?&](ref|source|utm_source|gad_source)=([^?&]+)/;\nconst utmCampaignRegex = /[?&](utm_campaign|gad_campaignid)=([^?&]+)/;\nconst utmMediumRegex = /[?&](utm_medium)=([^?&]+)/;\nconst utmTermRegex = /[?&](utm_term)=([^?&]+)/;\nconst utmContentRegex = /[?&](utm_content)=([^?&]+)/;\nconst gclidRegex = /[?&](gclid)=([^?&]+)/;\nconst getGclid = () => {\n    return findInSearch(gclidRegex) ? '<gclid>' : undefined;\n};\nexport const isInBrowser = () => {\n    return typeof window !== 'undefined';\n};\nexport const isLocalhost = () => {\n    return location?.hostname === 'localhost' || location?.hostname === '127.0.0.1' || location?.hostname === '';\n};\nexport const isAutomated = () => {\n    return navigator?.webdriver;\n};\nexport const getLocale = () => {\n    return typeof navigator.languages !== 'undefined' ? navigator.languages[0] : navigator.language;\n};\nexport const getTimezone = () => {\n    try {\n        return Intl.DateTimeFormat().resolvedOptions().timeZone;\n    }\n    catch (e) {\n        return;\n    }\n};\nexport const getReferrer = () => {\n    return document.referrer || undefined;\n};\nexport const getUTMSource = () => findInSearch(utmSourceRegex);\nexport const getUTMMedium = () => findInSearch(utmMediumRegex) || getGclid();\nexport const getUTMCampaign = () => findInSearch(utmCampaignRegex);\nexport const getUTMTerm = () => findInSearch(utmTermRegex);\nexport const getUTMContent = () => findInSearch(utmContentRegex);\n/**\n * Function used to track the current page (path) of the application.\n * Will work in cases where the path looks like:\n * - /path\n * - /#/path\n * - /path?search\n * - /path?search#hash\n * - /path#hash?search\n *\n * @param options - Options for the function.\n * @param options.hash - Whether to trigger on hash change.\n * @param options.search - Whether to trigger on search change.\n * @returns The path of the current page.\n */\nexport const getPath = (options) => {\n    let result = location.pathname || '';\n    if (options.hash) {\n        const hashIndex = location.hash.indexOf('?');\n        const hashString = hashIndex > -1 ? location.hash.substring(0, hashIndex) : location.hash;\n        result += hashString;\n    }\n    if (options.search) {\n        const hashIndex = location.hash.indexOf('?');\n        const searchString = location.search || (hashIndex > -1 ? location.hash.substring(hashIndex) : '');\n        result += searchString;\n    }\n    return result;\n};\n//# sourceMappingURL=utils.js.map"
  },
  {
    "path": "dist/swetrix.cjs.js",
    "content": "'use strict';\n\nconst findInSearch = (exp) => {\n    const res = location.search.match(exp);\n    return (res && res[2]) || undefined;\n};\nconst utmSourceRegex = /[?&](ref|source|utm_source|gad_source)=([^?&]+)/;\nconst utmCampaignRegex = /[?&](utm_campaign|gad_campaignid)=([^?&]+)/;\nconst utmMediumRegex = /[?&](utm_medium)=([^?&]+)/;\nconst utmTermRegex = /[?&](utm_term)=([^?&]+)/;\nconst utmContentRegex = /[?&](utm_content)=([^?&]+)/;\nconst gclidRegex = /[?&](gclid)=([^?&]+)/;\nconst getGclid = () => {\n    return findInSearch(gclidRegex) ? '<gclid>' : undefined;\n};\nconst isInBrowser = () => {\n    return typeof window !== 'undefined';\n};\nconst isLocalhost = () => {\n    return (location === null || location === void 0 ? void 0 : location.hostname) === 'localhost' || (location === null || location === void 0 ? void 0 : location.hostname) === '127.0.0.1' || (location === null || location === void 0 ? void 0 : location.hostname) === '';\n};\nconst isAutomated = () => {\n    return navigator === null || navigator === void 0 ? void 0 : navigator.webdriver;\n};\nconst getLocale = () => {\n    return typeof navigator.languages !== 'undefined' ? navigator.languages[0] : navigator.language;\n};\nconst getTimezone = () => {\n    try {\n        return Intl.DateTimeFormat().resolvedOptions().timeZone;\n    }\n    catch (e) {\n        return;\n    }\n};\nconst getReferrer = () => {\n    return document.referrer || undefined;\n};\nconst getUTMSource = () => findInSearch(utmSourceRegex);\nconst getUTMMedium = () => findInSearch(utmMediumRegex) || getGclid();\nconst getUTMCampaign = () => findInSearch(utmCampaignRegex);\nconst getUTMTerm = () => findInSearch(utmTermRegex);\nconst getUTMContent = () => findInSearch(utmContentRegex);\n/**\n * Function used to track the current page (path) of the application.\n * Will work in cases where the path looks like:\n * - /path\n * - /#/path\n * - /path?search\n * - /path?search#hash\n * - /path#hash?search\n *\n * @param options - Options for the function.\n * @param options.hash - Whether to trigger on hash change.\n * @param options.search - Whether to trigger on search change.\n * @returns The path of the current page.\n */\nconst getPath = (options) => {\n    let result = location.pathname || '';\n    if (options.hash) {\n        const hashIndex = location.hash.indexOf('?');\n        const hashString = hashIndex > -1 ? location.hash.substring(0, hashIndex) : location.hash;\n        result += hashString;\n    }\n    if (options.search) {\n        const hashIndex = location.hash.indexOf('?');\n        const searchString = location.search || (hashIndex > -1 ? location.hash.substring(hashIndex) : '');\n        result += searchString;\n    }\n    return result;\n};\n\nconst defaultActions = {\n    stop() { },\n};\nconst DEFAULT_API_HOST = 'https://api.swetrix.com/log';\nconst DEFAULT_API_BASE = 'https://api.swetrix.com';\n// Default cache duration: 5 minutes\nconst DEFAULT_CACHE_DURATION = 5 * 60 * 1000;\nclass Lib {\n    constructor(projectID, options) {\n        this.projectID = projectID;\n        this.options = options;\n        this.pageData = null;\n        this.pageViewsOptions = null;\n        this.errorsOptions = null;\n        this.perfStatsCollected = false;\n        this.activePage = null;\n        this.errorListenerExists = false;\n        this.cachedData = null;\n        this.trackPathChange = this.trackPathChange.bind(this);\n        this.heartbeat = this.heartbeat.bind(this);\n        this.captureError = this.captureError.bind(this);\n    }\n    captureError(event) {\n        var _a, _b, _c, _d;\n        if (typeof ((_a = this.errorsOptions) === null || _a === void 0 ? void 0 : _a.sampleRate) === 'number' && this.errorsOptions.sampleRate >= Math.random()) {\n            return;\n        }\n        this.submitError({\n            // The file in which error occured.\n            filename: event.filename,\n            // The line of code error occured on.\n            lineno: event.lineno,\n            // The column of code error occured on.\n            colno: event.colno,\n            // Name of the error, if not exists (i.e. it's a custom thrown error). The initial value of name is \"Error\", but just in case lets explicitly set it here too.\n            name: ((_b = event.error) === null || _b === void 0 ? void 0 : _b.name) || 'Error',\n            // Description of the error. By default, we use message from Error object, is it does not contain the error name\n            // (we want to split error name and message so we could group them together later in dashboard).\n            // If message in error object does not exist - lets use a message from the Error event itself.\n            message: ((_c = event.error) === null || _c === void 0 ? void 0 : _c.message) || event.message,\n            // Stack trace of the error, if available.\n            stackTrace: (_d = event.error) === null || _d === void 0 ? void 0 : _d.stack,\n        }, true);\n    }\n    trackErrors(options) {\n        if (this.errorListenerExists || !this.canTrack()) {\n            return defaultActions;\n        }\n        this.errorsOptions = options;\n        window.addEventListener('error', this.captureError);\n        this.errorListenerExists = true;\n        return {\n            stop: () => {\n                window.removeEventListener('error', this.captureError);\n                this.errorListenerExists = false;\n            },\n        };\n    }\n    submitError(payload, evokeCallback) {\n        var _a, _b, _c;\n        const privateData = {\n            pid: this.projectID,\n        };\n        const errorPayload = {\n            pg: this.activePage ||\n                getPath({\n                    hash: (_a = this.pageViewsOptions) === null || _a === void 0 ? void 0 : _a.hash,\n                    search: (_b = this.pageViewsOptions) === null || _b === void 0 ? void 0 : _b.search,\n                }),\n            lc: getLocale(),\n            tz: getTimezone(),\n            ...payload,\n        };\n        if (evokeCallback && ((_c = this.errorsOptions) === null || _c === void 0 ? void 0 : _c.callback)) {\n            const callbackResult = this.errorsOptions.callback(errorPayload);\n            if (callbackResult === false) {\n                return;\n            }\n            if (callbackResult && typeof callbackResult === 'object') {\n                Object.assign(errorPayload, callbackResult);\n            }\n        }\n        Object.assign(errorPayload, privateData);\n        this.sendRequest('error', errorPayload);\n    }\n    async track(event) {\n        var _a, _b, _c, _d;\n        if (!this.canTrack()) {\n            return;\n        }\n        const data = {\n            ...event,\n            pid: this.projectID,\n            pg: this.activePage ||\n                getPath({\n                    hash: (_a = this.pageViewsOptions) === null || _a === void 0 ? void 0 : _a.hash,\n                    search: (_b = this.pageViewsOptions) === null || _b === void 0 ? void 0 : _b.search,\n                }),\n            lc: getLocale(),\n            tz: getTimezone(),\n            ref: getReferrer(),\n            so: getUTMSource(),\n            me: getUTMMedium(),\n            ca: getUTMCampaign(),\n            te: getUTMTerm(),\n            co: getUTMContent(),\n            profileId: (_c = event.profileId) !== null && _c !== void 0 ? _c : (_d = this.options) === null || _d === void 0 ? void 0 : _d.profileId,\n        };\n        await this.sendRequest('custom', data);\n    }\n    trackPageViews(options) {\n        if (!this.canTrack()) {\n            return defaultActions;\n        }\n        if (this.pageData) {\n            return this.pageData.actions;\n        }\n        this.pageViewsOptions = options;\n        let interval;\n        if (!(options === null || options === void 0 ? void 0 : options.unique)) {\n            interval = setInterval(this.trackPathChange, 2000);\n        }\n        setTimeout(this.heartbeat, 3000);\n        const hbInterval = setInterval(this.heartbeat, 28000);\n        const path = getPath({\n            hash: options === null || options === void 0 ? void 0 : options.hash,\n            search: options === null || options === void 0 ? void 0 : options.search,\n        });\n        this.pageData = {\n            path,\n            actions: {\n                stop: () => {\n                    clearInterval(interval);\n                    clearInterval(hbInterval);\n                },\n            },\n        };\n        this.trackPage(path, options === null || options === void 0 ? void 0 : options.unique);\n        return this.pageData.actions;\n    }\n    getPerformanceStats() {\n        var _a;\n        if (!this.canTrack() || this.perfStatsCollected || !((_a = window.performance) === null || _a === void 0 ? void 0 : _a.getEntriesByType)) {\n            return {};\n        }\n        const perf = window.performance.getEntriesByType('navigation')[0];\n        if (!perf) {\n            return {};\n        }\n        this.perfStatsCollected = true;\n        return {\n            // Network\n            dns: perf.domainLookupEnd - perf.domainLookupStart, // DNS Resolution\n            tls: perf.secureConnectionStart ? perf.requestStart - perf.secureConnectionStart : 0, // TLS Setup; checking if secureConnectionStart is not 0 (it's 0 for non-https websites)\n            conn: perf.secureConnectionStart\n                ? perf.secureConnectionStart - perf.connectStart\n                : perf.connectEnd - perf.connectStart, // Connection time\n            response: perf.responseEnd - perf.responseStart, // Response Time (Download)\n            // Frontend\n            render: perf.domComplete - perf.domContentLoadedEventEnd, // Browser rendering the HTML time\n            dom_load: perf.domContentLoadedEventEnd - perf.responseEnd, // DOM loading timing\n            page_load: perf.loadEventStart, // Page load time\n            // Backend\n            ttfb: perf.responseStart - perf.requestStart,\n        };\n    }\n    /**\n     * Fetches all feature flags and experiments for the project.\n     * Results are cached for 5 minutes by default.\n     *\n     * @param options - Options for evaluating feature flags.\n     * @param forceRefresh - If true, bypasses the cache and fetches fresh data.\n     * @returns A promise that resolves to a record of flag keys to boolean values.\n     */\n    async getFeatureFlags(options, forceRefresh) {\n        var _a, _b, _c, _d;\n        if (!isInBrowser()) {\n            return {};\n        }\n        const requestedProfileId = (_a = options === null || options === void 0 ? void 0 : options.profileId) !== null && _a !== void 0 ? _a : (_b = this.options) === null || _b === void 0 ? void 0 : _b.profileId;\n        // Check cache first - must match profileId and not be expired\n        if (!forceRefresh && this.cachedData) {\n            const now = Date.now();\n            const isSameProfile = this.cachedData.profileId === requestedProfileId;\n            if (isSameProfile && now - this.cachedData.timestamp < DEFAULT_CACHE_DURATION) {\n                return this.cachedData.flags;\n            }\n        }\n        try {\n            await this.fetchFlagsAndExperiments(options);\n            return ((_c = this.cachedData) === null || _c === void 0 ? void 0 : _c.flags) || {};\n        }\n        catch (error) {\n            console.warn('[Swetrix] Error fetching feature flags:', error);\n            return ((_d = this.cachedData) === null || _d === void 0 ? void 0 : _d.flags) || {};\n        }\n    }\n    /**\n     * Internal method to fetch both feature flags and experiments from the API.\n     */\n    async fetchFlagsAndExperiments(options) {\n        var _a, _b, _c, _d;\n        const apiBase = this.getApiBase();\n        const body = {\n            pid: this.projectID,\n        };\n        // Use profileId from options, or fall back to global profileId\n        const profileId = (_a = options === null || options === void 0 ? void 0 : options.profileId) !== null && _a !== void 0 ? _a : (_b = this.options) === null || _b === void 0 ? void 0 : _b.profileId;\n        if (profileId) {\n            body.profileId = profileId;\n        }\n        const response = await fetch(`${apiBase}/feature-flag/evaluate`, {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json',\n            },\n            body: JSON.stringify(body),\n        });\n        if (!response.ok) {\n            console.warn('[Swetrix] Failed to fetch feature flags and experiments:', response.status);\n            return;\n        }\n        const data = (await response.json());\n        // Use profileId from options, or fall back to global profileId\n        const cachedProfileId = (_c = options === null || options === void 0 ? void 0 : options.profileId) !== null && _c !== void 0 ? _c : (_d = this.options) === null || _d === void 0 ? void 0 : _d.profileId;\n        // Update cache with both flags and experiments\n        this.cachedData = {\n            flags: data.flags || {},\n            experiments: data.experiments || {},\n            timestamp: Date.now(),\n            profileId: cachedProfileId,\n        };\n    }\n    /**\n     * Gets the value of a single feature flag.\n     *\n     * @param key - The feature flag key.\n     * @param options - Options for evaluating the feature flag.\n     * @param defaultValue - Default value to return if the flag is not found. Defaults to false.\n     * @returns A promise that resolves to the boolean value of the flag.\n     */\n    async getFeatureFlag(key, options, defaultValue = false) {\n        var _a;\n        const flags = await this.getFeatureFlags(options);\n        return (_a = flags[key]) !== null && _a !== void 0 ? _a : defaultValue;\n    }\n    /**\n     * Clears the cached feature flags and experiments, forcing a fresh fetch on the next call.\n     */\n    clearFeatureFlagsCache() {\n        this.cachedData = null;\n    }\n    /**\n     * Fetches all A/B test experiments for the project.\n     * Results are cached for 5 minutes by default (shared cache with feature flags).\n     *\n     * @param options - Options for evaluating experiments.\n     * @param forceRefresh - If true, bypasses the cache and fetches fresh data.\n     * @returns A promise that resolves to a record of experiment IDs to variant keys.\n     *\n     * @example\n     * ```typescript\n     * const experiments = await getExperiments()\n     * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }\n     * ```\n     */\n    async getExperiments(options, forceRefresh) {\n        var _a, _b, _c, _d;\n        if (!isInBrowser()) {\n            return {};\n        }\n        const requestedProfileId = (_a = options === null || options === void 0 ? void 0 : options.profileId) !== null && _a !== void 0 ? _a : (_b = this.options) === null || _b === void 0 ? void 0 : _b.profileId;\n        // Check cache first - must match profileId and not be expired\n        if (!forceRefresh && this.cachedData) {\n            const now = Date.now();\n            const isSameProfile = this.cachedData.profileId === requestedProfileId;\n            if (isSameProfile && now - this.cachedData.timestamp < DEFAULT_CACHE_DURATION) {\n                return this.cachedData.experiments;\n            }\n        }\n        try {\n            await this.fetchFlagsAndExperiments(options);\n            return ((_c = this.cachedData) === null || _c === void 0 ? void 0 : _c.experiments) || {};\n        }\n        catch (error) {\n            console.warn('[Swetrix] Error fetching experiments:', error);\n            return ((_d = this.cachedData) === null || _d === void 0 ? void 0 : _d.experiments) || {};\n        }\n    }\n    /**\n     * Gets the variant key for a specific A/B test experiment.\n     *\n     * @param experimentId - The experiment ID.\n     * @param options - Options for evaluating the experiment.\n     * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.\n     * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.\n     *\n     * @example\n     * ```typescript\n     * const variant = await getExperiment('checkout-redesign')\n     *\n     * if (variant === 'new-checkout') {\n     *   // Show new checkout flow\n     * } else {\n     *   // Show control (original) checkout\n     * }\n     * ```\n     */\n    async getExperiment(experimentId, options, defaultVariant = null) {\n        var _a;\n        const experiments = await this.getExperiments(options);\n        return (_a = experiments[experimentId]) !== null && _a !== void 0 ? _a : defaultVariant;\n    }\n    /**\n     * Clears the cached experiments (alias for clearFeatureFlagsCache since they share the same cache).\n     */\n    clearExperimentsCache() {\n        this.cachedData = null;\n    }\n    /**\n     * Gets the anonymous profile ID for the current visitor.\n     * If profileId was set via init options, returns that.\n     * Otherwise, requests server to generate one from IP/UA hash.\n     *\n     * This ID can be used for revenue attribution with payment providers.\n     *\n     * @returns A promise that resolves to the profile ID string, or null on error.\n     *\n     * @example\n     * ```typescript\n     * const profileId = await swetrix.getProfileId()\n     *\n     * // Pass to Paddle Checkout for revenue attribution\n     * Paddle.Checkout.open({\n     *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n     *   customData: {\n     *     swetrix_profile_id: profileId,\n     *     swetrix_session_id: await swetrix.getSessionId()\n     *   }\n     * })\n     * ```\n     */\n    async getProfileId() {\n        var _a;\n        // If profileId is already set in options, return it\n        if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.profileId) {\n            return this.options.profileId;\n        }\n        if (!isInBrowser()) {\n            return null;\n        }\n        try {\n            const apiBase = this.getApiBase();\n            const response = await fetch(`${apiBase}/log/profile-id`, {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json',\n                },\n                body: JSON.stringify({ pid: this.projectID }),\n            });\n            if (!response.ok) {\n                return null;\n            }\n            const data = (await response.json());\n            return data.profileId;\n        }\n        catch (_b) {\n            return null;\n        }\n    }\n    /**\n     * Gets the current session ID for the visitor.\n     * Session IDs are generated server-side based on IP and user agent.\n     *\n     * This ID can be used for revenue attribution with payment providers.\n     *\n     * @returns A promise that resolves to the session ID string, or null on error.\n     *\n     * @example\n     * ```typescript\n     * const sessionId = await swetrix.getSessionId()\n     *\n     * // Pass to Paddle Checkout for revenue attribution\n     * Paddle.Checkout.open({\n     *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n     *   customData: {\n     *     swetrix_profile_id: await swetrix.getProfileId(),\n     *     swetrix_session_id: sessionId\n     *   }\n     * })\n     * ```\n     */\n    async getSessionId() {\n        if (!isInBrowser()) {\n            return null;\n        }\n        try {\n            const apiBase = this.getApiBase();\n            const response = await fetch(`${apiBase}/log/session-id`, {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json',\n                },\n                body: JSON.stringify({ pid: this.projectID }),\n            });\n            if (!response.ok) {\n                return null;\n            }\n            const data = (await response.json());\n            return data.sessionId;\n        }\n        catch (_a) {\n            return null;\n        }\n    }\n    /**\n     * Gets the API base URL (without /log suffix).\n     */\n    getApiBase() {\n        var _a;\n        if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.apiURL) {\n            // Remove trailing /log if present\n            return this.options.apiURL.replace(/\\/log\\/?$/, '');\n        }\n        return DEFAULT_API_BASE;\n    }\n    heartbeat() {\n        var _a, _b;\n        if (!((_a = this.pageViewsOptions) === null || _a === void 0 ? void 0 : _a.heartbeatOnBackground) && document.visibilityState === 'hidden') {\n            return;\n        }\n        const data = {\n            pid: this.projectID,\n        };\n        if ((_b = this.options) === null || _b === void 0 ? void 0 : _b.profileId) {\n            data.profileId = this.options.profileId;\n        }\n        this.sendRequest('hb', data);\n    }\n    // Tracking path changes. If path changes -> calling this.trackPage method\n    trackPathChange() {\n        var _a, _b;\n        if (!this.pageData)\n            return;\n        const newPath = getPath({\n            hash: (_a = this.pageViewsOptions) === null || _a === void 0 ? void 0 : _a.hash,\n            search: (_b = this.pageViewsOptions) === null || _b === void 0 ? void 0 : _b.search,\n        });\n        const { path } = this.pageData;\n        if (path !== newPath) {\n            this.trackPage(newPath, false);\n        }\n    }\n    trackPage(pg, unique = false) {\n        if (!this.pageData)\n            return;\n        this.pageData.path = pg;\n        const perf = this.getPerformanceStats();\n        this.activePage = pg;\n        this.submitPageView({ pg }, unique, perf, true);\n    }\n    submitPageView(payload, unique, perf, evokeCallback) {\n        var _a, _b;\n        const privateData = {\n            pid: this.projectID,\n            perf,\n            unique,\n        };\n        const pvPayload = {\n            lc: getLocale(),\n            tz: getTimezone(),\n            ref: getReferrer(),\n            so: getUTMSource(),\n            me: getUTMMedium(),\n            ca: getUTMCampaign(),\n            te: getUTMTerm(),\n            co: getUTMContent(),\n            profileId: (_a = this.options) === null || _a === void 0 ? void 0 : _a.profileId,\n            ...payload,\n        };\n        if (evokeCallback && ((_b = this.pageViewsOptions) === null || _b === void 0 ? void 0 : _b.callback)) {\n            const callbackResult = this.pageViewsOptions.callback(pvPayload);\n            if (callbackResult === false) {\n                return;\n            }\n            if (callbackResult && typeof callbackResult === 'object') {\n                Object.assign(pvPayload, callbackResult);\n            }\n        }\n        Object.assign(pvPayload, privateData);\n        this.sendRequest('', pvPayload);\n    }\n    canTrack() {\n        var _a, _b, _c, _d;\n        if (((_a = this.options) === null || _a === void 0 ? void 0 : _a.disabled) ||\n            !isInBrowser() ||\n            (((_b = this.options) === null || _b === void 0 ? void 0 : _b.respectDNT) && ((_c = window.navigator) === null || _c === void 0 ? void 0 : _c.doNotTrack) === '1') ||\n            (!((_d = this.options) === null || _d === void 0 ? void 0 : _d.devMode) && isLocalhost()) ||\n            isAutomated()) {\n            return false;\n        }\n        return true;\n    }\n    async sendRequest(path, body) {\n        var _a;\n        const host = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.apiURL) || DEFAULT_API_HOST;\n        await fetch(`${host}/${path}`, {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json',\n            },\n            body: JSON.stringify(body),\n        });\n    }\n}\n\nexports.LIB_INSTANCE = null;\n/**\n * Initialise the tracking library instance (other methods won't work if the library is not initialised).\n *\n * @param {string} pid The Project ID to link the instance of Swetrix.js to.\n * @param {LibOptions} options Options related to the tracking.\n * @returns {Lib} Instance of the Swetrix.js.\n */\nfunction init(pid, options) {\n    if (!exports.LIB_INSTANCE) {\n        exports.LIB_INSTANCE = new Lib(pid, options);\n    }\n    return exports.LIB_INSTANCE;\n}\n/**\n * With this function you are able to track any custom events you want.\n * You should never send any identifiable data (like User ID, email, session cookie, etc.) as an event name.\n * The total number of track calls and their conversion rate will be saved.\n *\n * @param {TrackEventOptions} event The options related to the custom event.\n */\nasync function track(event) {\n    if (!exports.LIB_INSTANCE)\n        return;\n    await exports.LIB_INSTANCE.track(event);\n}\n/**\n * With this function you are able to automatically track pageviews across your application.\n *\n * @param {PageViewsOptions} options Pageviews tracking options.\n * @returns {PageActions} The actions related to the tracking. Used to stop tracking pages.\n */\nfunction trackViews(options) {\n    return new Promise((resolve) => {\n        if (!exports.LIB_INSTANCE) {\n            resolve(defaultActions);\n            return;\n        }\n        // We need to verify that document.readyState is complete for the performance stats to be collected correctly.\n        if (typeof document === 'undefined' || document.readyState === 'complete') {\n            resolve(exports.LIB_INSTANCE.trackPageViews(options));\n        }\n        else {\n            window.addEventListener('load', () => {\n                // @ts-ignore\n                resolve(exports.LIB_INSTANCE.trackPageViews(options));\n            });\n        }\n    });\n}\n/**\n * This function is used to set up automatic error events tracking.\n * It set's up an error listener, and whenever an error happens, it gets tracked.\n *\n * @returns {ErrorActions} The actions related to the tracking. Used to stop tracking errors.\n */\nfunction trackErrors(options) {\n    if (!exports.LIB_INSTANCE) {\n        return defaultActions;\n    }\n    return exports.LIB_INSTANCE.trackErrors(options);\n}\n/**\n * This function is used to manually track an error event.\n * It's useful if you want to track specific errors in your application.\n *\n * @param payload Swetrix error object to send.\n * @returns void\n */\nfunction trackError(payload) {\n    if (!exports.LIB_INSTANCE)\n        return;\n    exports.LIB_INSTANCE.submitError(payload, false);\n}\n/**\n * This function is used to manually track a page view event.\n * It's useful if your application uses esoteric routing which is not supported by Swetrix by default.\n *\n * @deprecated This function is deprecated and will be removed soon, please use the `pageview` instead.\n * @param pg Path of the page to track (this will be sent to the Swetrix API and displayed in the dashboard).\n * @param _prev Path of the previous page (deprecated and ignored).\n * @param unique If set to `true`, only 1 event with the same ID will be saved per user session.\n * @returns void\n */\nfunction trackPageview(pg, _prev, unique) {\n    if (!exports.LIB_INSTANCE)\n        return;\n    exports.LIB_INSTANCE.submitPageView({ pg }, Boolean(unique), {});\n}\nfunction pageview(options) {\n    if (!exports.LIB_INSTANCE)\n        return;\n    exports.LIB_INSTANCE.submitPageView(options.payload, Boolean(options.unique), {});\n}\n/**\n * Fetches all feature flags for the project.\n * Results are cached for 5 minutes by default.\n *\n * @param options - Options for evaluating feature flags (visitorId, attributes).\n * @param forceRefresh - If true, bypasses the cache and fetches fresh flags.\n * @returns A promise that resolves to a record of flag keys to boolean values.\n *\n * @example\n * ```typescript\n * const flags = await getFeatureFlags({\n *   visitorId: 'user-123',\n *   attributes: { cc: 'US', dv: 'desktop' }\n * })\n *\n * if (flags['new-checkout']) {\n *   // Show new checkout flow\n * }\n * ```\n */\nasync function getFeatureFlags(options, forceRefresh) {\n    if (!exports.LIB_INSTANCE)\n        return {};\n    return exports.LIB_INSTANCE.getFeatureFlags(options, forceRefresh);\n}\n/**\n * Gets the value of a single feature flag.\n *\n * @param key - The feature flag key.\n * @param options - Options for evaluating the feature flag (visitorId, attributes).\n * @param defaultValue - Default value to return if the flag is not found. Defaults to false.\n * @returns A promise that resolves to the boolean value of the flag.\n *\n * @example\n * ```typescript\n * const isEnabled = await getFeatureFlag('dark-mode', { visitorId: 'user-123' })\n *\n * if (isEnabled) {\n *   // Enable dark mode\n * }\n * ```\n */\nasync function getFeatureFlag(key, options, defaultValue = false) {\n    if (!exports.LIB_INSTANCE)\n        return defaultValue;\n    return exports.LIB_INSTANCE.getFeatureFlag(key, options, defaultValue);\n}\n/**\n * Clears the cached feature flags, forcing a fresh fetch on the next call.\n * Useful when you know the user's context has changed significantly.\n */\nfunction clearFeatureFlagsCache() {\n    if (!exports.LIB_INSTANCE)\n        return;\n    exports.LIB_INSTANCE.clearFeatureFlagsCache();\n}\n/**\n * Fetches all A/B test experiments for the project.\n * Results are cached for 5 minutes by default (shared cache with feature flags).\n *\n * @param options - Options for evaluating experiments.\n * @param forceRefresh - If true, bypasses the cache and fetches fresh data.\n * @returns A promise that resolves to a record of experiment IDs to variant keys.\n *\n * @example\n * ```typescript\n * const experiments = await getExperiments()\n * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }\n *\n * // Use the assigned variant\n * const checkoutVariant = experiments['checkout-experiment-id']\n * if (checkoutVariant === 'new-checkout') {\n *   showNewCheckout()\n * } else {\n *   showOriginalCheckout()\n * }\n * ```\n */\nasync function getExperiments(options, forceRefresh) {\n    if (!exports.LIB_INSTANCE)\n        return {};\n    return exports.LIB_INSTANCE.getExperiments(options, forceRefresh);\n}\n/**\n * Gets the variant key for a specific A/B test experiment.\n *\n * @param experimentId - The experiment ID.\n * @param options - Options for evaluating the experiment.\n * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.\n * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.\n *\n * @example\n * ```typescript\n * const variant = await getExperiment('checkout-redesign-experiment-id')\n *\n * if (variant === 'new-checkout') {\n *   // Show new checkout flow\n *   showNewCheckout()\n * } else if (variant === 'control') {\n *   // Show original checkout (control group)\n *   showOriginalCheckout()\n * } else {\n *   // Experiment not running or user not included\n *   showOriginalCheckout()\n * }\n * ```\n */\nasync function getExperiment(experimentId, options, defaultVariant = null) {\n    if (!exports.LIB_INSTANCE)\n        return defaultVariant;\n    return exports.LIB_INSTANCE.getExperiment(experimentId, options, defaultVariant);\n}\n/**\n * Clears the cached experiments, forcing a fresh fetch on the next call.\n * This is an alias for clearFeatureFlagsCache since experiments and flags share the same cache.\n */\nfunction clearExperimentsCache() {\n    if (!exports.LIB_INSTANCE)\n        return;\n    exports.LIB_INSTANCE.clearExperimentsCache();\n}\n/**\n * Gets the anonymous profile ID for the current visitor.\n * If profileId was set via init options, returns that.\n * Otherwise, requests server to generate one from IP/UA hash.\n *\n * This ID can be used for revenue attribution with payment providers like Paddle.\n *\n * @returns A promise that resolves to the profile ID string, or null on error.\n *\n * @example\n * ```typescript\n * const profileId = await getProfileId()\n *\n * // Pass to Paddle Checkout for revenue attribution\n * Paddle.Checkout.open({\n *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n *   customData: {\n *     swetrix_profile_id: profileId,\n *     swetrix_session_id: await getSessionId()\n *   }\n * })\n * ```\n */\nasync function getProfileId() {\n    if (!exports.LIB_INSTANCE)\n        return null;\n    return exports.LIB_INSTANCE.getProfileId();\n}\n/**\n * Gets the current session ID for the visitor.\n * Session IDs are generated server-side based on IP and user agent.\n *\n * This ID can be used for revenue attribution with payment providers like Paddle.\n *\n * @returns A promise that resolves to the session ID string, or null on error.\n *\n * @example\n * ```typescript\n * const sessionId = await getSessionId()\n *\n * // Pass to Paddle Checkout for revenue attribution\n * Paddle.Checkout.open({\n *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n *   customData: {\n *     swetrix_profile_id: await getProfileId(),\n *     swetrix_session_id: sessionId\n *   }\n * })\n * ```\n */\nasync function getSessionId() {\n    if (!exports.LIB_INSTANCE)\n        return null;\n    return exports.LIB_INSTANCE.getSessionId();\n}\n\nexports.clearExperimentsCache = clearExperimentsCache;\nexports.clearFeatureFlagsCache = clearFeatureFlagsCache;\nexports.getExperiment = getExperiment;\nexports.getExperiments = getExperiments;\nexports.getFeatureFlag = getFeatureFlag;\nexports.getFeatureFlags = getFeatureFlags;\nexports.getProfileId = getProfileId;\nexports.getSessionId = getSessionId;\nexports.init = init;\nexports.pageview = pageview;\nexports.track = track;\nexports.trackError = trackError;\nexports.trackErrors = trackErrors;\nexports.trackPageview = trackPageview;\nexports.trackViews = trackViews;\n//# sourceMappingURL=swetrix.cjs.js.map\n"
  },
  {
    "path": "dist/swetrix.es5.js",
    "content": "const findInSearch = (exp) => {\n    const res = location.search.match(exp);\n    return (res && res[2]) || undefined;\n};\nconst utmSourceRegex = /[?&](ref|source|utm_source|gad_source)=([^?&]+)/;\nconst utmCampaignRegex = /[?&](utm_campaign|gad_campaignid)=([^?&]+)/;\nconst utmMediumRegex = /[?&](utm_medium)=([^?&]+)/;\nconst utmTermRegex = /[?&](utm_term)=([^?&]+)/;\nconst utmContentRegex = /[?&](utm_content)=([^?&]+)/;\nconst gclidRegex = /[?&](gclid)=([^?&]+)/;\nconst getGclid = () => {\n    return findInSearch(gclidRegex) ? '<gclid>' : undefined;\n};\nconst isInBrowser = () => {\n    return typeof window !== 'undefined';\n};\nconst isLocalhost = () => {\n    return (location === null || location === void 0 ? void 0 : location.hostname) === 'localhost' || (location === null || location === void 0 ? void 0 : location.hostname) === '127.0.0.1' || (location === null || location === void 0 ? void 0 : location.hostname) === '';\n};\nconst isAutomated = () => {\n    return navigator === null || navigator === void 0 ? void 0 : navigator.webdriver;\n};\nconst getLocale = () => {\n    return typeof navigator.languages !== 'undefined' ? navigator.languages[0] : navigator.language;\n};\nconst getTimezone = () => {\n    try {\n        return Intl.DateTimeFormat().resolvedOptions().timeZone;\n    }\n    catch (e) {\n        return;\n    }\n};\nconst getReferrer = () => {\n    return document.referrer || undefined;\n};\nconst getUTMSource = () => findInSearch(utmSourceRegex);\nconst getUTMMedium = () => findInSearch(utmMediumRegex) || getGclid();\nconst getUTMCampaign = () => findInSearch(utmCampaignRegex);\nconst getUTMTerm = () => findInSearch(utmTermRegex);\nconst getUTMContent = () => findInSearch(utmContentRegex);\n/**\n * Function used to track the current page (path) of the application.\n * Will work in cases where the path looks like:\n * - /path\n * - /#/path\n * - /path?search\n * - /path?search#hash\n * - /path#hash?search\n *\n * @param options - Options for the function.\n * @param options.hash - Whether to trigger on hash change.\n * @param options.search - Whether to trigger on search change.\n * @returns The path of the current page.\n */\nconst getPath = (options) => {\n    let result = location.pathname || '';\n    if (options.hash) {\n        const hashIndex = location.hash.indexOf('?');\n        const hashString = hashIndex > -1 ? location.hash.substring(0, hashIndex) : location.hash;\n        result += hashString;\n    }\n    if (options.search) {\n        const hashIndex = location.hash.indexOf('?');\n        const searchString = location.search || (hashIndex > -1 ? location.hash.substring(hashIndex) : '');\n        result += searchString;\n    }\n    return result;\n};\n\nconst defaultActions = {\n    stop() { },\n};\nconst DEFAULT_API_HOST = 'https://api.swetrix.com/log';\nconst DEFAULT_API_BASE = 'https://api.swetrix.com';\n// Default cache duration: 5 minutes\nconst DEFAULT_CACHE_DURATION = 5 * 60 * 1000;\nclass Lib {\n    constructor(projectID, options) {\n        this.projectID = projectID;\n        this.options = options;\n        this.pageData = null;\n        this.pageViewsOptions = null;\n        this.errorsOptions = null;\n        this.perfStatsCollected = false;\n        this.activePage = null;\n        this.errorListenerExists = false;\n        this.cachedData = null;\n        this.trackPathChange = this.trackPathChange.bind(this);\n        this.heartbeat = this.heartbeat.bind(this);\n        this.captureError = this.captureError.bind(this);\n    }\n    captureError(event) {\n        var _a, _b, _c, _d;\n        if (typeof ((_a = this.errorsOptions) === null || _a === void 0 ? void 0 : _a.sampleRate) === 'number' && this.errorsOptions.sampleRate >= Math.random()) {\n            return;\n        }\n        this.submitError({\n            // The file in which error occured.\n            filename: event.filename,\n            // The line of code error occured on.\n            lineno: event.lineno,\n            // The column of code error occured on.\n            colno: event.colno,\n            // Name of the error, if not exists (i.e. it's a custom thrown error). The initial value of name is \"Error\", but just in case lets explicitly set it here too.\n            name: ((_b = event.error) === null || _b === void 0 ? void 0 : _b.name) || 'Error',\n            // Description of the error. By default, we use message from Error object, is it does not contain the error name\n            // (we want to split error name and message so we could group them together later in dashboard).\n            // If message in error object does not exist - lets use a message from the Error event itself.\n            message: ((_c = event.error) === null || _c === void 0 ? void 0 : _c.message) || event.message,\n            // Stack trace of the error, if available.\n            stackTrace: (_d = event.error) === null || _d === void 0 ? void 0 : _d.stack,\n        }, true);\n    }\n    trackErrors(options) {\n        if (this.errorListenerExists || !this.canTrack()) {\n            return defaultActions;\n        }\n        this.errorsOptions = options;\n        window.addEventListener('error', this.captureError);\n        this.errorListenerExists = true;\n        return {\n            stop: () => {\n                window.removeEventListener('error', this.captureError);\n                this.errorListenerExists = false;\n            },\n        };\n    }\n    submitError(payload, evokeCallback) {\n        var _a, _b, _c;\n        const privateData = {\n            pid: this.projectID,\n        };\n        const errorPayload = {\n            pg: this.activePage ||\n                getPath({\n                    hash: (_a = this.pageViewsOptions) === null || _a === void 0 ? void 0 : _a.hash,\n                    search: (_b = this.pageViewsOptions) === null || _b === void 0 ? void 0 : _b.search,\n                }),\n            lc: getLocale(),\n            tz: getTimezone(),\n            ...payload,\n        };\n        if (evokeCallback && ((_c = this.errorsOptions) === null || _c === void 0 ? void 0 : _c.callback)) {\n            const callbackResult = this.errorsOptions.callback(errorPayload);\n            if (callbackResult === false) {\n                return;\n            }\n            if (callbackResult && typeof callbackResult === 'object') {\n                Object.assign(errorPayload, callbackResult);\n            }\n        }\n        Object.assign(errorPayload, privateData);\n        this.sendRequest('error', errorPayload);\n    }\n    async track(event) {\n        var _a, _b, _c, _d;\n        if (!this.canTrack()) {\n            return;\n        }\n        const data = {\n            ...event,\n            pid: this.projectID,\n            pg: this.activePage ||\n                getPath({\n                    hash: (_a = this.pageViewsOptions) === null || _a === void 0 ? void 0 : _a.hash,\n                    search: (_b = this.pageViewsOptions) === null || _b === void 0 ? void 0 : _b.search,\n                }),\n            lc: getLocale(),\n            tz: getTimezone(),\n            ref: getReferrer(),\n            so: getUTMSource(),\n            me: getUTMMedium(),\n            ca: getUTMCampaign(),\n            te: getUTMTerm(),\n            co: getUTMContent(),\n            profileId: (_c = event.profileId) !== null && _c !== void 0 ? _c : (_d = this.options) === null || _d === void 0 ? void 0 : _d.profileId,\n        };\n        await this.sendRequest('custom', data);\n    }\n    trackPageViews(options) {\n        if (!this.canTrack()) {\n            return defaultActions;\n        }\n        if (this.pageData) {\n            return this.pageData.actions;\n        }\n        this.pageViewsOptions = options;\n        let interval;\n        if (!(options === null || options === void 0 ? void 0 : options.unique)) {\n            interval = setInterval(this.trackPathChange, 2000);\n        }\n        setTimeout(this.heartbeat, 3000);\n        const hbInterval = setInterval(this.heartbeat, 28000);\n        const path = getPath({\n            hash: options === null || options === void 0 ? void 0 : options.hash,\n            search: options === null || options === void 0 ? void 0 : options.search,\n        });\n        this.pageData = {\n            path,\n            actions: {\n                stop: () => {\n                    clearInterval(interval);\n                    clearInterval(hbInterval);\n                },\n            },\n        };\n        this.trackPage(path, options === null || options === void 0 ? void 0 : options.unique);\n        return this.pageData.actions;\n    }\n    getPerformanceStats() {\n        var _a;\n        if (!this.canTrack() || this.perfStatsCollected || !((_a = window.performance) === null || _a === void 0 ? void 0 : _a.getEntriesByType)) {\n            return {};\n        }\n        const perf = window.performance.getEntriesByType('navigation')[0];\n        if (!perf) {\n            return {};\n        }\n        this.perfStatsCollected = true;\n        return {\n            // Network\n            dns: perf.domainLookupEnd - perf.domainLookupStart, // DNS Resolution\n            tls: perf.secureConnectionStart ? perf.requestStart - perf.secureConnectionStart : 0, // TLS Setup; checking if secureConnectionStart is not 0 (it's 0 for non-https websites)\n            conn: perf.secureConnectionStart\n                ? perf.secureConnectionStart - perf.connectStart\n                : perf.connectEnd - perf.connectStart, // Connection time\n            response: perf.responseEnd - perf.responseStart, // Response Time (Download)\n            // Frontend\n            render: perf.domComplete - perf.domContentLoadedEventEnd, // Browser rendering the HTML time\n            dom_load: perf.domContentLoadedEventEnd - perf.responseEnd, // DOM loading timing\n            page_load: perf.loadEventStart, // Page load time\n            // Backend\n            ttfb: perf.responseStart - perf.requestStart,\n        };\n    }\n    /**\n     * Fetches all feature flags and experiments for the project.\n     * Results are cached for 5 minutes by default.\n     *\n     * @param options - Options for evaluating feature flags.\n     * @param forceRefresh - If true, bypasses the cache and fetches fresh data.\n     * @returns A promise that resolves to a record of flag keys to boolean values.\n     */\n    async getFeatureFlags(options, forceRefresh) {\n        var _a, _b, _c, _d;\n        if (!isInBrowser()) {\n            return {};\n        }\n        const requestedProfileId = (_a = options === null || options === void 0 ? void 0 : options.profileId) !== null && _a !== void 0 ? _a : (_b = this.options) === null || _b === void 0 ? void 0 : _b.profileId;\n        // Check cache first - must match profileId and not be expired\n        if (!forceRefresh && this.cachedData) {\n            const now = Date.now();\n            const isSameProfile = this.cachedData.profileId === requestedProfileId;\n            if (isSameProfile && now - this.cachedData.timestamp < DEFAULT_CACHE_DURATION) {\n                return this.cachedData.flags;\n            }\n        }\n        try {\n            await this.fetchFlagsAndExperiments(options);\n            return ((_c = this.cachedData) === null || _c === void 0 ? void 0 : _c.flags) || {};\n        }\n        catch (error) {\n            console.warn('[Swetrix] Error fetching feature flags:', error);\n            return ((_d = this.cachedData) === null || _d === void 0 ? void 0 : _d.flags) || {};\n        }\n    }\n    /**\n     * Internal method to fetch both feature flags and experiments from the API.\n     */\n    async fetchFlagsAndExperiments(options) {\n        var _a, _b, _c, _d;\n        const apiBase = this.getApiBase();\n        const body = {\n            pid: this.projectID,\n        };\n        // Use profileId from options, or fall back to global profileId\n        const profileId = (_a = options === null || options === void 0 ? void 0 : options.profileId) !== null && _a !== void 0 ? _a : (_b = this.options) === null || _b === void 0 ? void 0 : _b.profileId;\n        if (profileId) {\n            body.profileId = profileId;\n        }\n        const response = await fetch(`${apiBase}/feature-flag/evaluate`, {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json',\n            },\n            body: JSON.stringify(body),\n        });\n        if (!response.ok) {\n            console.warn('[Swetrix] Failed to fetch feature flags and experiments:', response.status);\n            return;\n        }\n        const data = (await response.json());\n        // Use profileId from options, or fall back to global profileId\n        const cachedProfileId = (_c = options === null || options === void 0 ? void 0 : options.profileId) !== null && _c !== void 0 ? _c : (_d = this.options) === null || _d === void 0 ? void 0 : _d.profileId;\n        // Update cache with both flags and experiments\n        this.cachedData = {\n            flags: data.flags || {},\n            experiments: data.experiments || {},\n            timestamp: Date.now(),\n            profileId: cachedProfileId,\n        };\n    }\n    /**\n     * Gets the value of a single feature flag.\n     *\n     * @param key - The feature flag key.\n     * @param options - Options for evaluating the feature flag.\n     * @param defaultValue - Default value to return if the flag is not found. Defaults to false.\n     * @returns A promise that resolves to the boolean value of the flag.\n     */\n    async getFeatureFlag(key, options, defaultValue = false) {\n        var _a;\n        const flags = await this.getFeatureFlags(options);\n        return (_a = flags[key]) !== null && _a !== void 0 ? _a : defaultValue;\n    }\n    /**\n     * Clears the cached feature flags and experiments, forcing a fresh fetch on the next call.\n     */\n    clearFeatureFlagsCache() {\n        this.cachedData = null;\n    }\n    /**\n     * Fetches all A/B test experiments for the project.\n     * Results are cached for 5 minutes by default (shared cache with feature flags).\n     *\n     * @param options - Options for evaluating experiments.\n     * @param forceRefresh - If true, bypasses the cache and fetches fresh data.\n     * @returns A promise that resolves to a record of experiment IDs to variant keys.\n     *\n     * @example\n     * ```typescript\n     * const experiments = await getExperiments()\n     * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }\n     * ```\n     */\n    async getExperiments(options, forceRefresh) {\n        var _a, _b, _c, _d;\n        if (!isInBrowser()) {\n            return {};\n        }\n        const requestedProfileId = (_a = options === null || options === void 0 ? void 0 : options.profileId) !== null && _a !== void 0 ? _a : (_b = this.options) === null || _b === void 0 ? void 0 : _b.profileId;\n        // Check cache first - must match profileId and not be expired\n        if (!forceRefresh && this.cachedData) {\n            const now = Date.now();\n            const isSameProfile = this.cachedData.profileId === requestedProfileId;\n            if (isSameProfile && now - this.cachedData.timestamp < DEFAULT_CACHE_DURATION) {\n                return this.cachedData.experiments;\n            }\n        }\n        try {\n            await this.fetchFlagsAndExperiments(options);\n            return ((_c = this.cachedData) === null || _c === void 0 ? void 0 : _c.experiments) || {};\n        }\n        catch (error) {\n            console.warn('[Swetrix] Error fetching experiments:', error);\n            return ((_d = this.cachedData) === null || _d === void 0 ? void 0 : _d.experiments) || {};\n        }\n    }\n    /**\n     * Gets the variant key for a specific A/B test experiment.\n     *\n     * @param experimentId - The experiment ID.\n     * @param options - Options for evaluating the experiment.\n     * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.\n     * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.\n     *\n     * @example\n     * ```typescript\n     * const variant = await getExperiment('checkout-redesign')\n     *\n     * if (variant === 'new-checkout') {\n     *   // Show new checkout flow\n     * } else {\n     *   // Show control (original) checkout\n     * }\n     * ```\n     */\n    async getExperiment(experimentId, options, defaultVariant = null) {\n        var _a;\n        const experiments = await this.getExperiments(options);\n        return (_a = experiments[experimentId]) !== null && _a !== void 0 ? _a : defaultVariant;\n    }\n    /**\n     * Clears the cached experiments (alias for clearFeatureFlagsCache since they share the same cache).\n     */\n    clearExperimentsCache() {\n        this.cachedData = null;\n    }\n    /**\n     * Gets the anonymous profile ID for the current visitor.\n     * If profileId was set via init options, returns that.\n     * Otherwise, requests server to generate one from IP/UA hash.\n     *\n     * This ID can be used for revenue attribution with payment providers.\n     *\n     * @returns A promise that resolves to the profile ID string, or null on error.\n     *\n     * @example\n     * ```typescript\n     * const profileId = await swetrix.getProfileId()\n     *\n     * // Pass to Paddle Checkout for revenue attribution\n     * Paddle.Checkout.open({\n     *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n     *   customData: {\n     *     swetrix_profile_id: profileId,\n     *     swetrix_session_id: await swetrix.getSessionId()\n     *   }\n     * })\n     * ```\n     */\n    async getProfileId() {\n        var _a;\n        // If profileId is already set in options, return it\n        if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.profileId) {\n            return this.options.profileId;\n        }\n        if (!isInBrowser()) {\n            return null;\n        }\n        try {\n            const apiBase = this.getApiBase();\n            const response = await fetch(`${apiBase}/log/profile-id`, {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json',\n                },\n                body: JSON.stringify({ pid: this.projectID }),\n            });\n            if (!response.ok) {\n                return null;\n            }\n            const data = (await response.json());\n            return data.profileId;\n        }\n        catch (_b) {\n            return null;\n        }\n    }\n    /**\n     * Gets the current session ID for the visitor.\n     * Session IDs are generated server-side based on IP and user agent.\n     *\n     * This ID can be used for revenue attribution with payment providers.\n     *\n     * @returns A promise that resolves to the session ID string, or null on error.\n     *\n     * @example\n     * ```typescript\n     * const sessionId = await swetrix.getSessionId()\n     *\n     * // Pass to Paddle Checkout for revenue attribution\n     * Paddle.Checkout.open({\n     *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n     *   customData: {\n     *     swetrix_profile_id: await swetrix.getProfileId(),\n     *     swetrix_session_id: sessionId\n     *   }\n     * })\n     * ```\n     */\n    async getSessionId() {\n        if (!isInBrowser()) {\n            return null;\n        }\n        try {\n            const apiBase = this.getApiBase();\n            const response = await fetch(`${apiBase}/log/session-id`, {\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json',\n                },\n                body: JSON.stringify({ pid: this.projectID }),\n            });\n            if (!response.ok) {\n                return null;\n            }\n            const data = (await response.json());\n            return data.sessionId;\n        }\n        catch (_a) {\n            return null;\n        }\n    }\n    /**\n     * Gets the API base URL (without /log suffix).\n     */\n    getApiBase() {\n        var _a;\n        if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.apiURL) {\n            // Remove trailing /log if present\n            return this.options.apiURL.replace(/\\/log\\/?$/, '');\n        }\n        return DEFAULT_API_BASE;\n    }\n    heartbeat() {\n        var _a, _b;\n        if (!((_a = this.pageViewsOptions) === null || _a === void 0 ? void 0 : _a.heartbeatOnBackground) && document.visibilityState === 'hidden') {\n            return;\n        }\n        const data = {\n            pid: this.projectID,\n        };\n        if ((_b = this.options) === null || _b === void 0 ? void 0 : _b.profileId) {\n            data.profileId = this.options.profileId;\n        }\n        this.sendRequest('hb', data);\n    }\n    // Tracking path changes. If path changes -> calling this.trackPage method\n    trackPathChange() {\n        var _a, _b;\n        if (!this.pageData)\n            return;\n        const newPath = getPath({\n            hash: (_a = this.pageViewsOptions) === null || _a === void 0 ? void 0 : _a.hash,\n            search: (_b = this.pageViewsOptions) === null || _b === void 0 ? void 0 : _b.search,\n        });\n        const { path } = this.pageData;\n        if (path !== newPath) {\n            this.trackPage(newPath, false);\n        }\n    }\n    trackPage(pg, unique = false) {\n        if (!this.pageData)\n            return;\n        this.pageData.path = pg;\n        const perf = this.getPerformanceStats();\n        this.activePage = pg;\n        this.submitPageView({ pg }, unique, perf, true);\n    }\n    submitPageView(payload, unique, perf, evokeCallback) {\n        var _a, _b;\n        const privateData = {\n            pid: this.projectID,\n            perf,\n            unique,\n        };\n        const pvPayload = {\n            lc: getLocale(),\n            tz: getTimezone(),\n            ref: getReferrer(),\n            so: getUTMSource(),\n            me: getUTMMedium(),\n            ca: getUTMCampaign(),\n            te: getUTMTerm(),\n            co: getUTMContent(),\n            profileId: (_a = this.options) === null || _a === void 0 ? void 0 : _a.profileId,\n            ...payload,\n        };\n        if (evokeCallback && ((_b = this.pageViewsOptions) === null || _b === void 0 ? void 0 : _b.callback)) {\n            const callbackResult = this.pageViewsOptions.callback(pvPayload);\n            if (callbackResult === false) {\n                return;\n            }\n            if (callbackResult && typeof callbackResult === 'object') {\n                Object.assign(pvPayload, callbackResult);\n            }\n        }\n        Object.assign(pvPayload, privateData);\n        this.sendRequest('', pvPayload);\n    }\n    canTrack() {\n        var _a, _b, _c, _d;\n        if (((_a = this.options) === null || _a === void 0 ? void 0 : _a.disabled) ||\n            !isInBrowser() ||\n            (((_b = this.options) === null || _b === void 0 ? void 0 : _b.respectDNT) && ((_c = window.navigator) === null || _c === void 0 ? void 0 : _c.doNotTrack) === '1') ||\n            (!((_d = this.options) === null || _d === void 0 ? void 0 : _d.devMode) && isLocalhost()) ||\n            isAutomated()) {\n            return false;\n        }\n        return true;\n    }\n    async sendRequest(path, body) {\n        var _a;\n        const host = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.apiURL) || DEFAULT_API_HOST;\n        await fetch(`${host}/${path}`, {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json',\n            },\n            body: JSON.stringify(body),\n        });\n    }\n}\n\nlet LIB_INSTANCE = null;\n/**\n * Initialise the tracking library instance (other methods won't work if the library is not initialised).\n *\n * @param {string} pid The Project ID to link the instance of Swetrix.js to.\n * @param {LibOptions} options Options related to the tracking.\n * @returns {Lib} Instance of the Swetrix.js.\n */\nfunction init(pid, options) {\n    if (!LIB_INSTANCE) {\n        LIB_INSTANCE = new Lib(pid, options);\n    }\n    return LIB_INSTANCE;\n}\n/**\n * With this function you are able to track any custom events you want.\n * You should never send any identifiable data (like User ID, email, session cookie, etc.) as an event name.\n * The total number of track calls and their conversion rate will be saved.\n *\n * @param {TrackEventOptions} event The options related to the custom event.\n */\nasync function track(event) {\n    if (!LIB_INSTANCE)\n        return;\n    await LIB_INSTANCE.track(event);\n}\n/**\n * With this function you are able to automatically track pageviews across your application.\n *\n * @param {PageViewsOptions} options Pageviews tracking options.\n * @returns {PageActions} The actions related to the tracking. Used to stop tracking pages.\n */\nfunction trackViews(options) {\n    return new Promise((resolve) => {\n        if (!LIB_INSTANCE) {\n            resolve(defaultActions);\n            return;\n        }\n        // We need to verify that document.readyState is complete for the performance stats to be collected correctly.\n        if (typeof document === 'undefined' || document.readyState === 'complete') {\n            resolve(LIB_INSTANCE.trackPageViews(options));\n        }\n        else {\n            window.addEventListener('load', () => {\n                // @ts-ignore\n                resolve(LIB_INSTANCE.trackPageViews(options));\n            });\n        }\n    });\n}\n/**\n * This function is used to set up automatic error events tracking.\n * It set's up an error listener, and whenever an error happens, it gets tracked.\n *\n * @returns {ErrorActions} The actions related to the tracking. Used to stop tracking errors.\n */\nfunction trackErrors(options) {\n    if (!LIB_INSTANCE) {\n        return defaultActions;\n    }\n    return LIB_INSTANCE.trackErrors(options);\n}\n/**\n * This function is used to manually track an error event.\n * It's useful if you want to track specific errors in your application.\n *\n * @param payload Swetrix error object to send.\n * @returns void\n */\nfunction trackError(payload) {\n    if (!LIB_INSTANCE)\n        return;\n    LIB_INSTANCE.submitError(payload, false);\n}\n/**\n * This function is used to manually track a page view event.\n * It's useful if your application uses esoteric routing which is not supported by Swetrix by default.\n *\n * @deprecated This function is deprecated and will be removed soon, please use the `pageview` instead.\n * @param pg Path of the page to track (this will be sent to the Swetrix API and displayed in the dashboard).\n * @param _prev Path of the previous page (deprecated and ignored).\n * @param unique If set to `true`, only 1 event with the same ID will be saved per user session.\n * @returns void\n */\nfunction trackPageview(pg, _prev, unique) {\n    if (!LIB_INSTANCE)\n        return;\n    LIB_INSTANCE.submitPageView({ pg }, Boolean(unique), {});\n}\nfunction pageview(options) {\n    if (!LIB_INSTANCE)\n        return;\n    LIB_INSTANCE.submitPageView(options.payload, Boolean(options.unique), {});\n}\n/**\n * Fetches all feature flags for the project.\n * Results are cached for 5 minutes by default.\n *\n * @param options - Options for evaluating feature flags (visitorId, attributes).\n * @param forceRefresh - If true, bypasses the cache and fetches fresh flags.\n * @returns A promise that resolves to a record of flag keys to boolean values.\n *\n * @example\n * ```typescript\n * const flags = await getFeatureFlags({\n *   visitorId: 'user-123',\n *   attributes: { cc: 'US', dv: 'desktop' }\n * })\n *\n * if (flags['new-checkout']) {\n *   // Show new checkout flow\n * }\n * ```\n */\nasync function getFeatureFlags(options, forceRefresh) {\n    if (!LIB_INSTANCE)\n        return {};\n    return LIB_INSTANCE.getFeatureFlags(options, forceRefresh);\n}\n/**\n * Gets the value of a single feature flag.\n *\n * @param key - The feature flag key.\n * @param options - Options for evaluating the feature flag (visitorId, attributes).\n * @param defaultValue - Default value to return if the flag is not found. Defaults to false.\n * @returns A promise that resolves to the boolean value of the flag.\n *\n * @example\n * ```typescript\n * const isEnabled = await getFeatureFlag('dark-mode', { visitorId: 'user-123' })\n *\n * if (isEnabled) {\n *   // Enable dark mode\n * }\n * ```\n */\nasync function getFeatureFlag(key, options, defaultValue = false) {\n    if (!LIB_INSTANCE)\n        return defaultValue;\n    return LIB_INSTANCE.getFeatureFlag(key, options, defaultValue);\n}\n/**\n * Clears the cached feature flags, forcing a fresh fetch on the next call.\n * Useful when you know the user's context has changed significantly.\n */\nfunction clearFeatureFlagsCache() {\n    if (!LIB_INSTANCE)\n        return;\n    LIB_INSTANCE.clearFeatureFlagsCache();\n}\n/**\n * Fetches all A/B test experiments for the project.\n * Results are cached for 5 minutes by default (shared cache with feature flags).\n *\n * @param options - Options for evaluating experiments.\n * @param forceRefresh - If true, bypasses the cache and fetches fresh data.\n * @returns A promise that resolves to a record of experiment IDs to variant keys.\n *\n * @example\n * ```typescript\n * const experiments = await getExperiments()\n * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }\n *\n * // Use the assigned variant\n * const checkoutVariant = experiments['checkout-experiment-id']\n * if (checkoutVariant === 'new-checkout') {\n *   showNewCheckout()\n * } else {\n *   showOriginalCheckout()\n * }\n * ```\n */\nasync function getExperiments(options, forceRefresh) {\n    if (!LIB_INSTANCE)\n        return {};\n    return LIB_INSTANCE.getExperiments(options, forceRefresh);\n}\n/**\n * Gets the variant key for a specific A/B test experiment.\n *\n * @param experimentId - The experiment ID.\n * @param options - Options for evaluating the experiment.\n * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.\n * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.\n *\n * @example\n * ```typescript\n * const variant = await getExperiment('checkout-redesign-experiment-id')\n *\n * if (variant === 'new-checkout') {\n *   // Show new checkout flow\n *   showNewCheckout()\n * } else if (variant === 'control') {\n *   // Show original checkout (control group)\n *   showOriginalCheckout()\n * } else {\n *   // Experiment not running or user not included\n *   showOriginalCheckout()\n * }\n * ```\n */\nasync function getExperiment(experimentId, options, defaultVariant = null) {\n    if (!LIB_INSTANCE)\n        return defaultVariant;\n    return LIB_INSTANCE.getExperiment(experimentId, options, defaultVariant);\n}\n/**\n * Clears the cached experiments, forcing a fresh fetch on the next call.\n * This is an alias for clearFeatureFlagsCache since experiments and flags share the same cache.\n */\nfunction clearExperimentsCache() {\n    if (!LIB_INSTANCE)\n        return;\n    LIB_INSTANCE.clearExperimentsCache();\n}\n/**\n * Gets the anonymous profile ID for the current visitor.\n * If profileId was set via init options, returns that.\n * Otherwise, requests server to generate one from IP/UA hash.\n *\n * This ID can be used for revenue attribution with payment providers like Paddle.\n *\n * @returns A promise that resolves to the profile ID string, or null on error.\n *\n * @example\n * ```typescript\n * const profileId = await getProfileId()\n *\n * // Pass to Paddle Checkout for revenue attribution\n * Paddle.Checkout.open({\n *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n *   customData: {\n *     swetrix_profile_id: profileId,\n *     swetrix_session_id: await getSessionId()\n *   }\n * })\n * ```\n */\nasync function getProfileId() {\n    if (!LIB_INSTANCE)\n        return null;\n    return LIB_INSTANCE.getProfileId();\n}\n/**\n * Gets the current session ID for the visitor.\n * Session IDs are generated server-side based on IP and user agent.\n *\n * This ID can be used for revenue attribution with payment providers like Paddle.\n *\n * @returns A promise that resolves to the session ID string, or null on error.\n *\n * @example\n * ```typescript\n * const sessionId = await getSessionId()\n *\n * // Pass to Paddle Checkout for revenue attribution\n * Paddle.Checkout.open({\n *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n *   customData: {\n *     swetrix_profile_id: await getProfileId(),\n *     swetrix_session_id: sessionId\n *   }\n * })\n * ```\n */\nasync function getSessionId() {\n    if (!LIB_INSTANCE)\n        return null;\n    return LIB_INSTANCE.getSessionId();\n}\n\nexport { LIB_INSTANCE, clearExperimentsCache, clearFeatureFlagsCache, getExperiment, getExperiments, getFeatureFlag, getFeatureFlags, getProfileId, getSessionId, init, pageview, track, trackError, trackErrors, trackPageview, trackViews };\n//# sourceMappingURL=swetrix.es5.js.map\n"
  },
  {
    "path": "dist/swetrix.js",
    "content": "!function(t,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?e(exports):\"function\"==typeof define&&define.amd?define([\"exports\"],e):e((t=\"undefined\"!=typeof globalThis?globalThis:t||self).swetrix={})}(this,(function(t){\"use strict\";const e=t=>{const e=location.search.match(t);return e&&e[2]||void 0},i=/[?&](ref|source|utm_source|gad_source)=([^?&]+)/,a=/[?&](utm_campaign|gad_campaignid)=([^?&]+)/,n=/[?&](utm_medium)=([^?&]+)/,o=/[?&](utm_term)=([^?&]+)/,r=/[?&](utm_content)=([^?&]+)/,s=/[?&](gclid)=([^?&]+)/,l=()=>\"undefined\"!=typeof window,c=()=>void 0!==navigator.languages?navigator.languages[0]:navigator.language,d=()=>{try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch(t){return}},h=()=>document.referrer||void 0,u=()=>e(i),p=()=>e(n)||(e(s)?\"<gclid>\":void 0),v=()=>e(a),g=()=>e(o),f=()=>e(r),I=t=>{let e=location.pathname||\"\";if(t.hash){const t=location.hash.indexOf(\"?\");e+=t>-1?location.hash.substring(0,t):location.hash}if(t.search){const t=location.hash.indexOf(\"?\");e+=location.search||(t>-1?location.hash.substring(t):\"\")}return e},m={stop(){}},E=3e5;class N{constructor(t,e){this.projectID=t,this.options=e,this.pageData=null,this.pageViewsOptions=null,this.errorsOptions=null,this.perfStatsCollected=!1,this.activePage=null,this.errorListenerExists=!1,this.cachedData=null,this.trackPathChange=this.trackPathChange.bind(this),this.heartbeat=this.heartbeat.bind(this),this.captureError=this.captureError.bind(this)}captureError(t){var e,i,a,n;\"number\"==typeof(null===(e=this.errorsOptions)||void 0===e?void 0:e.sampleRate)&&this.errorsOptions.sampleRate>=Math.random()||this.submitError({filename:t.filename,lineno:t.lineno,colno:t.colno,name:(null===(i=t.error)||void 0===i?void 0:i.name)||\"Error\",message:(null===(a=t.error)||void 0===a?void 0:a.message)||t.message,stackTrace:null===(n=t.error)||void 0===n?void 0:n.stack},!0)}trackErrors(t){return this.errorListenerExists||!this.canTrack()?m:(this.errorsOptions=t,window.addEventListener(\"error\",this.captureError),this.errorListenerExists=!0,{stop:()=>{window.removeEventListener(\"error\",this.captureError),this.errorListenerExists=!1}})}submitError(t,e){var i,a,n;const o={pid:this.projectID},r={pg:this.activePage||I({hash:null===(i=this.pageViewsOptions)||void 0===i?void 0:i.hash,search:null===(a=this.pageViewsOptions)||void 0===a?void 0:a.search}),lc:c(),tz:d(),...t};if(e&&(null===(n=this.errorsOptions)||void 0===n?void 0:n.callback)){const t=this.errorsOptions.callback(r);if(!1===t)return;t&&\"object\"==typeof t&&Object.assign(r,t)}Object.assign(r,o),this.sendRequest(\"error\",r)}async track(t){var e,i,a,n;if(!this.canTrack())return;const o={...t,pid:this.projectID,pg:this.activePage||I({hash:null===(e=this.pageViewsOptions)||void 0===e?void 0:e.hash,search:null===(i=this.pageViewsOptions)||void 0===i?void 0:i.search}),lc:c(),tz:d(),ref:h(),so:u(),me:p(),ca:v(),te:g(),co:f(),profileId:null!==(a=t.profileId)&&void 0!==a?a:null===(n=this.options)||void 0===n?void 0:n.profileId};await this.sendRequest(\"custom\",o)}trackPageViews(t){if(!this.canTrack())return m;if(this.pageData)return this.pageData.actions;let e;this.pageViewsOptions=t,(null==t?void 0:t.unique)||(e=setInterval(this.trackPathChange,2e3)),setTimeout(this.heartbeat,3e3);const i=setInterval(this.heartbeat,28e3),a=I({hash:null==t?void 0:t.hash,search:null==t?void 0:t.search});return this.pageData={path:a,actions:{stop:()=>{clearInterval(e),clearInterval(i)}}},this.trackPage(a,null==t?void 0:t.unique),this.pageData.actions}getPerformanceStats(){var t;if(!this.canTrack()||this.perfStatsCollected||!(null===(t=window.performance)||void 0===t?void 0:t.getEntriesByType))return{};const e=window.performance.getEntriesByType(\"navigation\")[0];return e?(this.perfStatsCollected=!0,{dns:e.domainLookupEnd-e.domainLookupStart,tls:e.secureConnectionStart?e.requestStart-e.secureConnectionStart:0,conn:e.secureConnectionStart?e.secureConnectionStart-e.connectStart:e.connectEnd-e.connectStart,response:e.responseEnd-e.responseStart,render:e.domComplete-e.domContentLoadedEventEnd,dom_load:e.domContentLoadedEventEnd-e.responseEnd,page_load:e.loadEventStart,ttfb:e.responseStart-e.requestStart}):{}}async getFeatureFlags(t,e){var i,a,n,o;if(!l())return{};const r=null!==(i=null==t?void 0:t.profileId)&&void 0!==i?i:null===(a=this.options)||void 0===a?void 0:a.profileId;if(!e&&this.cachedData){const t=Date.now();if(this.cachedData.profileId===r&&t-this.cachedData.timestamp<E)return this.cachedData.flags}try{return await this.fetchFlagsAndExperiments(t),(null===(n=this.cachedData)||void 0===n?void 0:n.flags)||{}}catch(t){return console.warn(\"[Swetrix] Error fetching feature flags:\",t),(null===(o=this.cachedData)||void 0===o?void 0:o.flags)||{}}}async fetchFlagsAndExperiments(t){var e,i,a,n;const o=this.getApiBase(),r={pid:this.projectID},s=null!==(e=null==t?void 0:t.profileId)&&void 0!==e?e:null===(i=this.options)||void 0===i?void 0:i.profileId;s&&(r.profileId=s);const l=await fetch(`${o}/feature-flag/evaluate`,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify(r)});if(!l.ok)return void console.warn(\"[Swetrix] Failed to fetch feature flags and experiments:\",l.status);const c=await l.json(),d=null!==(a=null==t?void 0:t.profileId)&&void 0!==a?a:null===(n=this.options)||void 0===n?void 0:n.profileId;this.cachedData={flags:c.flags||{},experiments:c.experiments||{},timestamp:Date.now(),profileId:d}}async getFeatureFlag(t,e,i=!1){var a;return null!==(a=(await this.getFeatureFlags(e))[t])&&void 0!==a?a:i}clearFeatureFlagsCache(){this.cachedData=null}async getExperiments(t,e){var i,a,n,o;if(!l())return{};const r=null!==(i=null==t?void 0:t.profileId)&&void 0!==i?i:null===(a=this.options)||void 0===a?void 0:a.profileId;if(!e&&this.cachedData){const t=Date.now();if(this.cachedData.profileId===r&&t-this.cachedData.timestamp<E)return this.cachedData.experiments}try{return await this.fetchFlagsAndExperiments(t),(null===(n=this.cachedData)||void 0===n?void 0:n.experiments)||{}}catch(t){return console.warn(\"[Swetrix] Error fetching experiments:\",t),(null===(o=this.cachedData)||void 0===o?void 0:o.experiments)||{}}}async getExperiment(t,e,i=null){var a;return null!==(a=(await this.getExperiments(e))[t])&&void 0!==a?a:i}clearExperimentsCache(){this.cachedData=null}async getProfileId(){var t;if(null===(t=this.options)||void 0===t?void 0:t.profileId)return this.options.profileId;if(!l())return null;try{const t=this.getApiBase(),e=await fetch(`${t}/log/profile-id`,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({pid:this.projectID})});if(!e.ok)return null;return(await e.json()).profileId}catch(t){return null}}async getSessionId(){if(!l())return null;try{const t=this.getApiBase(),e=await fetch(`${t}/log/session-id`,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify({pid:this.projectID})});if(!e.ok)return null;return(await e.json()).sessionId}catch(t){return null}}getApiBase(){var t;return(null===(t=this.options)||void 0===t?void 0:t.apiURL)?this.options.apiURL.replace(/\\/log\\/?$/,\"\"):\"https://api.swetrix.com\"}heartbeat(){var t,e;if(!(null===(t=this.pageViewsOptions)||void 0===t?void 0:t.heartbeatOnBackground)&&\"hidden\"===document.visibilityState)return;const i={pid:this.projectID};(null===(e=this.options)||void 0===e?void 0:e.profileId)&&(i.profileId=this.options.profileId),this.sendRequest(\"hb\",i)}trackPathChange(){var t,e;if(!this.pageData)return;const i=I({hash:null===(t=this.pageViewsOptions)||void 0===t?void 0:t.hash,search:null===(e=this.pageViewsOptions)||void 0===e?void 0:e.search}),{path:a}=this.pageData;a!==i&&this.trackPage(i,!1)}trackPage(t,e=!1){if(!this.pageData)return;this.pageData.path=t;const i=this.getPerformanceStats();this.activePage=t,this.submitPageView({pg:t},e,i,!0)}submitPageView(t,e,i,a){var n,o;const r={pid:this.projectID,perf:i,unique:e},s={lc:c(),tz:d(),ref:h(),so:u(),me:p(),ca:v(),te:g(),co:f(),profileId:null===(n=this.options)||void 0===n?void 0:n.profileId,...t};if(a&&(null===(o=this.pageViewsOptions)||void 0===o?void 0:o.callback)){const t=this.pageViewsOptions.callback(s);if(!1===t)return;t&&\"object\"==typeof t&&Object.assign(s,t)}Object.assign(s,r),this.sendRequest(\"\",s)}canTrack(){var t,e,i,a;return!((null===(t=this.options)||void 0===t?void 0:t.disabled)||!l()||(null===(e=this.options)||void 0===e?void 0:e.respectDNT)&&\"1\"===(null===(i=window.navigator)||void 0===i?void 0:i.doNotTrack)||!(null===(a=this.options)||void 0===a?void 0:a.devMode)&&(\"localhost\"===(null===location||void 0===location?void 0:location.hostname)||\"127.0.0.1\"===(null===location||void 0===location?void 0:location.hostname)||\"\"===(null===location||void 0===location?void 0:location.hostname))||(null===navigator||void 0===navigator?void 0:navigator.webdriver))}async sendRequest(t,e){var i;const a=(null===(i=this.options)||void 0===i?void 0:i.apiURL)||\"https://api.swetrix.com/log\";await fetch(`${a}/${t}`,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:JSON.stringify(e)})}}t.LIB_INSTANCE=null,t.clearExperimentsCache=function(){t.LIB_INSTANCE&&t.LIB_INSTANCE.clearExperimentsCache()},t.clearFeatureFlagsCache=function(){t.LIB_INSTANCE&&t.LIB_INSTANCE.clearFeatureFlagsCache()},t.getExperiment=async function(e,i,a=null){return t.LIB_INSTANCE?t.LIB_INSTANCE.getExperiment(e,i,a):a},t.getExperiments=async function(e,i){return t.LIB_INSTANCE?t.LIB_INSTANCE.getExperiments(e,i):{}},t.getFeatureFlag=async function(e,i,a=!1){return t.LIB_INSTANCE?t.LIB_INSTANCE.getFeatureFlag(e,i,a):a},t.getFeatureFlags=async function(e,i){return t.LIB_INSTANCE?t.LIB_INSTANCE.getFeatureFlags(e,i):{}},t.getProfileId=async function(){return t.LIB_INSTANCE?t.LIB_INSTANCE.getProfileId():null},t.getSessionId=async function(){return t.LIB_INSTANCE?t.LIB_INSTANCE.getSessionId():null},t.init=function(e,i){return t.LIB_INSTANCE||(t.LIB_INSTANCE=new N(e,i)),t.LIB_INSTANCE},t.pageview=function(e){t.LIB_INSTANCE&&t.LIB_INSTANCE.submitPageView(e.payload,Boolean(e.unique),{})},t.track=async function(e){t.LIB_INSTANCE&&await t.LIB_INSTANCE.track(e)},t.trackError=function(e){t.LIB_INSTANCE&&t.LIB_INSTANCE.submitError(e,!1)},t.trackErrors=function(e){return t.LIB_INSTANCE?t.LIB_INSTANCE.trackErrors(e):m},t.trackPageview=function(e,i,a){t.LIB_INSTANCE&&t.LIB_INSTANCE.submitPageView({pg:e},Boolean(a),{})},t.trackViews=function(e){return new Promise((i=>{t.LIB_INSTANCE?\"undefined\"==typeof document||\"complete\"===document.readyState?i(t.LIB_INSTANCE.trackPageViews(e)):window.addEventListener(\"load\",(()=>{i(t.LIB_INSTANCE.trackPageViews(e))})):i(m)}))}}));\n//# sourceMappingURL=swetrix.js.map\n"
  },
  {
    "path": "jest.config.js",
    "content": "export default {\n  preset: 'ts-jest',\n  testEnvironment: 'jsdom',\n  extensionsToTreatAsEsm: ['.ts'],\n  moduleNameMapper: {\n    '^(\\\\.{1,2}/.*)\\\\.js$': '$1',\n  },\n  transform: {\n    '^.+\\\\.tsx?$': [\n      'ts-jest',\n      {\n        useESM: true,\n      },\n    ],\n  },\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"swetrix\",\n  \"version\": \"4.1.0\",\n  \"description\": \"The JavaScript analytics client for Swetrix Analytics\",\n  \"type\": \"module\",\n  \"main\": \"dist/swetrix.cjs.js\",\n  \"module\": \"dist/swetrix.es5.js\",\n  \"browser\": \"dist/swetrix.js\",\n  \"esnext\": \"dist/esnext/index.js\",\n  \"typings\": \"dist/esnext/index.d.ts\",\n  \"scripts\": {\n    \"prebuild\": \"rimraf dist\",\n    \"prepublish\": \"npm run build\",\n    \"build\": \"rollup -c && npm run tsc\",\n    \"start\": \"rollup -c -w\",\n    \"tsc\": \"tsc -p tsconfig.esnext.json\",\n    \"test\": \"jest\",\n    \"test:watch\": \"jest --watch\",\n    \"test:coverage\": \"jest --coverage\"\n  },\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/esnext/index.js\",\n      \"require\": \"./dist/swetrix.cjs.js\",\n      \"types\": \"./dist/esnext/index.d.ts\",\n      \"default\": \"./dist/swetrix.js\"\n    }\n  },\n  \"keywords\": [\n    \"swetrix\",\n    \"analytics\",\n    \"monitoring\",\n    \"metrics\",\n    \"privacy\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/Swetrix/swetrix-js.git\"\n  },\n  \"author\": \"Andrii Romasiun, Swetrix Ltd. <contact@swetrix.com>\",\n  \"funding\": \"https://github.com/sponsors/Swetrix\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/Swetrix/swetrix-js/issues\"\n  },\n  \"homepage\": \"https://docs.swetrix.com\",\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^28.0.3\",\n    \"@rollup/plugin-node-resolve\": \"^16.0.1\",\n    \"@rollup/plugin-terser\": \"^0.4.4\",\n    \"@rollup/plugin-typescript\": \"^12.1.2\",\n    \"@types/jest\": \"^29.5.14\",\n    \"@types/node\": \"^22.15.21\",\n    \"jest\": \"^29.7.0\",\n    \"jest-environment-jsdom\": \"^29.7.0\",\n    \"rimraf\": \"^6.0.1\",\n    \"rollup\": \"^4.41.0\",\n    \"tslib\": \"^2.6.3\",\n    \"ts-jest\": \"^29.3.4\",\n    \"typescript\": \"^5.8.3\"\n  },\n  \"devEngines\": {\n    \"runtime\": {\n      \"name\": \"node\",\n      \"onFail\": \"error\",\n      \"version\": \">=22\"\n    }\n  }\n}\n"
  },
  {
    "path": "rollup.config.mjs",
    "content": "import { nodeResolve } from '@rollup/plugin-node-resolve'\nimport commonjs from '@rollup/plugin-commonjs'\nimport typescript from '@rollup/plugin-typescript'\nimport terser from '@rollup/plugin-terser'\nimport pkg from './package.json' with { type: 'json' }\nimport { createRequire } from 'node:module'\n\nconst require = createRequire(import.meta.url)\n\nexport default [\n  {\n    input: 'src/index.ts',\n    output: [\n      { file: pkg.main, format: 'cjs', sourcemap: true },\n      { file: pkg.module, format: 'es', sourcemap: true },\n    ],\n    plugins: [\n      typescript({\n        outDir: './dist',\n        sourceMap: true,\n        tslib: require.resolve('tslib'),\n      }),\n      nodeResolve(),\n      commonjs(),\n    ],\n  },\n  {\n    input: 'src/index.ts',\n    output: [{ file: pkg.browser, format: 'umd', name: 'swetrix', sourcemap: true }],\n    plugins: [\n      typescript({\n        outDir: './dist',\n        sourceMap: true,\n        tslib: require.resolve('tslib'),\n      }),\n      nodeResolve(),\n      commonjs(),\n      terser(),\n    ],\n  },\n]\n"
  },
  {
    "path": "src/Lib.ts",
    "content": "import {\n  isInBrowser,\n  isLocalhost,\n  isAutomated,\n  getLocale,\n  getTimezone,\n  getReferrer,\n  getUTMCampaign,\n  getUTMMedium,\n  getUTMSource,\n  getUTMTerm,\n  getUTMContent,\n  getPath,\n} from './utils.js'\n\nexport interface LibOptions {\n  /**\n   * When set to `true`, localhost events will be sent to server.\n   */\n  devMode?: boolean\n\n  /**\n   * When set to `true`, the tracking library won't send any data to server.\n   * Useful for development purposes when this value is set based on `.env` var.\n   */\n  disabled?: boolean\n\n  /**\n   * By setting this flag to `true`, we will not collect ANY kind of data about the user with the DNT setting.\n   */\n  respectDNT?: boolean\n\n  /** Set a custom URL of the API server (for selfhosted variants of Swetrix). */\n  apiURL?: string\n\n  /**\n   * Optional profile ID for long-term user tracking.\n   * If set, it will be used for all pageviews and events unless overridden per-call.\n   */\n  profileId?: string\n}\n\nexport interface TrackEventOptions {\n  /** The custom event name. */\n  ev: string\n\n  /** If set to `true`, only 1 event with the same ID will be saved per user session. */\n  unique?: boolean\n\n  /** Event-related metadata object with string values. */\n  meta?: {\n    [key: string]: string | number | boolean | null | undefined\n  }\n\n  /** Optional profile ID for long-term user tracking. Overrides the global profileId if set. */\n  profileId?: string\n}\n\n// Partial user-editable pageview payload\nexport interface IPageViewPayload {\n  lc?: string\n  tz?: string\n  ref?: string\n  so?: string\n  me?: string\n  ca?: string\n  te?: string\n  co?: string\n  pg?: string | null\n\n  /** Pageview-related metadata object with string values. */\n  meta?: {\n    [key: string]: string | number | boolean | null | undefined\n  }\n\n  /** Optional profile ID for long-term user tracking. Overrides the global profileId if set. */\n  profileId?: string\n}\n\n// Partial user-editable error payload\nexport interface IErrorEventPayload {\n  name: string\n  message?: string | null\n  lineno?: number | null\n  colno?: number | null\n  filename?: string | null\n  stackTrace?: string | null\n  meta?: {\n    [key: string]: string | number | boolean | null | undefined\n  }\n}\n\nexport interface IInternalErrorEventPayload extends IErrorEventPayload {\n  lc?: string\n  tz?: string\n  pg?: string | null\n}\n\ninterface IPerfPayload {\n  dns: number\n  tls: number\n  conn: number\n  response: number\n  render: number\n  dom_load: number\n  page_load: number\n  ttfb: number\n}\n\n/**\n * Options for evaluating feature flags.\n */\nexport interface FeatureFlagsOptions {\n  /**\n   * Optional profile ID for long-term user tracking.\n   * If not provided, an anonymous profile ID will be generated server-side based on IP and user agent.\n   * Overrides the global profileId if set.\n   */\n  profileId?: string\n}\n\n/**\n * Options for evaluating experiments.\n */\nexport interface ExperimentOptions {\n  /**\n   * Optional profile ID for long-term user tracking.\n   * If not provided, an anonymous profile ID will be generated server-side based on IP and user agent.\n   * Overrides the global profileId if set.\n   */\n  profileId?: string\n}\n\n/**\n * Cached feature flags and experiments with timestamp.\n */\ninterface CachedData {\n  flags: Record<string, boolean>\n  experiments: Record<string, string>\n  timestamp: number\n  /** The profileId used when fetching this cached data */\n  profileId?: string\n}\n\n/**\n * The object returned by `trackPageViews()`, used to stop tracking pages.\n */\nexport interface PageActions {\n  /** Stops the tracking of pages. */\n  stop: () => void\n}\n\n/**\n * The object returned by `trackErrors()`, used to stop tracking errors.\n */\nexport interface ErrorActions {\n  /** Stops the tracking of errors. */\n  stop: () => void\n}\n\nexport interface PageData {\n  /** Current URL path. */\n  path: string\n\n  /** The object returned by `trackPageViews()`, used to stop tracking pages. */\n  actions: PageActions\n}\n\nexport interface ErrorOptions {\n  /**\n   * A number that indicates how many errors should be sent to the server.\n   * Accepts values between 0 and 1. For example, if set to 0.5 - only ~50% of errors will be sent to Swetrix.\n   * For testing, we recommend setting this value to 1. For production, you should configure it depending on your needs as each error event counts towards your plan.\n   *\n   * The default value for this option is 1.\n   */\n  sampleRate?: number\n\n  /**\n   * Callback to edit / prevent sending errors.\n   *\n   * @param payload - The error payload.\n   * @returns The edited payload or `false` to prevent sending the error event. If `true` is returned, the payload will be sent as-is.\n   */\n  callback?: (payload: IInternalErrorEventPayload) => Partial<IInternalErrorEventPayload> | boolean\n}\n\nexport interface PageViewsOptions {\n  /**\n   * If set to `true`, only unique events will be saved.\n   * This param is useful when tracking single-page landing websites.\n   */\n  unique?: boolean\n\n  /** Send Heartbeat requests when the website tab is not active in the browser. */\n  heartbeatOnBackground?: boolean\n\n  /**\n   * Set to `true` to enable hash-based routing.\n   * For example if you have pages like /#/path or want to track pages like /path#hash\n   */\n  hash?: boolean\n\n  /**\n   * Set to `true` to enable search-based routing.\n   * For example if you have pages like /path?search\n   */\n  search?: boolean\n\n  /**\n   * Callback to edit / prevent sending pageviews.\n   *\n   * @param payload - The pageview payload.\n   * @returns The edited payload or `false` to prevent sending the pageview. If `true` is returned, the payload will be sent as-is.\n   */\n  callback?: (payload: IPageViewPayload) => Partial<IPageViewPayload> | boolean\n}\n\nexport const defaultActions = {\n  stop() {},\n}\n\nconst DEFAULT_API_HOST = 'https://api.swetrix.com/log'\nconst DEFAULT_API_BASE = 'https://api.swetrix.com'\n\n// Default cache duration: 5 minutes\nconst DEFAULT_CACHE_DURATION = 5 * 60 * 1000\n\nexport class Lib {\n  private pageData: PageData | null = null\n  private pageViewsOptions?: PageViewsOptions | null = null\n  private errorsOptions?: ErrorOptions | null = null\n  private perfStatsCollected: boolean = false\n  private activePage: string | null = null\n  private errorListenerExists = false\n  private cachedData: CachedData | null = null\n\n  constructor(private projectID: string, private options?: LibOptions) {\n    this.trackPathChange = this.trackPathChange.bind(this)\n    this.heartbeat = this.heartbeat.bind(this)\n    this.captureError = this.captureError.bind(this)\n  }\n\n  captureError(event: ErrorEvent): void {\n    if (typeof this.errorsOptions?.sampleRate === 'number' && this.errorsOptions.sampleRate >= Math.random()) {\n      return\n    }\n\n    this.submitError(\n      {\n        // The file in which error occured.\n        filename: event.filename,\n\n        // The line of code error occured on.\n        lineno: event.lineno,\n\n        // The column of code error occured on.\n        colno: event.colno,\n\n        // Name of the error, if not exists (i.e. it's a custom thrown error). The initial value of name is \"Error\", but just in case lets explicitly set it here too.\n        name: event.error?.name || 'Error',\n\n        // Description of the error. By default, we use message from Error object, is it does not contain the error name\n        // (we want to split error name and message so we could group them together later in dashboard).\n        // If message in error object does not exist - lets use a message from the Error event itself.\n        message: event.error?.message || event.message,\n\n        // Stack trace of the error, if available.\n        stackTrace: event.error?.stack,\n      },\n      true,\n    )\n  }\n\n  trackErrors(options?: ErrorOptions): ErrorActions {\n    if (this.errorListenerExists || !this.canTrack()) {\n      return defaultActions\n    }\n\n    this.errorsOptions = options\n\n    window.addEventListener('error', this.captureError)\n    this.errorListenerExists = true\n\n    return {\n      stop: () => {\n        window.removeEventListener('error', this.captureError)\n        this.errorListenerExists = false\n      },\n    }\n  }\n\n  submitError(payload: IErrorEventPayload, evokeCallback?: boolean): void {\n    const privateData = {\n      pid: this.projectID,\n    }\n\n    const errorPayload = {\n      pg:\n        this.activePage ||\n        getPath({\n          hash: this.pageViewsOptions?.hash,\n          search: this.pageViewsOptions?.search,\n        }),\n      lc: getLocale(),\n      tz: getTimezone(),\n      ...payload,\n    }\n\n    if (evokeCallback && this.errorsOptions?.callback) {\n      const callbackResult = this.errorsOptions.callback(errorPayload)\n\n      if (callbackResult === false) {\n        return\n      }\n\n      if (callbackResult && typeof callbackResult === 'object') {\n        Object.assign(errorPayload, callbackResult)\n      }\n    }\n\n    Object.assign(errorPayload, privateData)\n\n    this.sendRequest('error', errorPayload)\n  }\n\n  async track(event: TrackEventOptions): Promise<void> {\n    if (!this.canTrack()) {\n      return\n    }\n\n    const data = {\n      ...event,\n      pid: this.projectID,\n      pg:\n        this.activePage ||\n        getPath({\n          hash: this.pageViewsOptions?.hash,\n          search: this.pageViewsOptions?.search,\n        }),\n      lc: getLocale(),\n      tz: getTimezone(),\n      ref: getReferrer(),\n      so: getUTMSource(),\n      me: getUTMMedium(),\n      ca: getUTMCampaign(),\n      te: getUTMTerm(),\n      co: getUTMContent(),\n      profileId: event.profileId ?? this.options?.profileId,\n    }\n    await this.sendRequest('custom', data)\n  }\n\n  trackPageViews(options?: PageViewsOptions): PageActions {\n    if (!this.canTrack()) {\n      return defaultActions\n    }\n\n    if (this.pageData) {\n      return this.pageData.actions\n    }\n\n    this.pageViewsOptions = options\n    let interval: NodeJS.Timeout\n\n    if (!options?.unique) {\n      interval = setInterval(this.trackPathChange, 2000)\n    }\n\n    setTimeout(this.heartbeat, 3000)\n    const hbInterval = setInterval(this.heartbeat, 28000)\n\n    const path = getPath({\n      hash: options?.hash,\n      search: options?.search,\n    })\n\n    this.pageData = {\n      path,\n      actions: {\n        stop: () => {\n          clearInterval(interval)\n          clearInterval(hbInterval)\n        },\n      },\n    }\n\n    this.trackPage(path, options?.unique)\n    return this.pageData.actions\n  }\n\n  getPerformanceStats(): IPerfPayload | {} {\n    if (!this.canTrack() || this.perfStatsCollected || !window.performance?.getEntriesByType) {\n      return {}\n    }\n\n    const perf = window.performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming\n\n    if (!perf) {\n      return {}\n    }\n\n    this.perfStatsCollected = true\n\n    return {\n      // Network\n      dns: perf.domainLookupEnd - perf.domainLookupStart, // DNS Resolution\n      tls: perf.secureConnectionStart ? perf.requestStart - perf.secureConnectionStart : 0, // TLS Setup; checking if secureConnectionStart is not 0 (it's 0 for non-https websites)\n      conn: perf.secureConnectionStart\n        ? perf.secureConnectionStart - perf.connectStart\n        : perf.connectEnd - perf.connectStart, // Connection time\n      response: perf.responseEnd - perf.responseStart, // Response Time (Download)\n\n      // Frontend\n      render: perf.domComplete - perf.domContentLoadedEventEnd, // Browser rendering the HTML time\n      dom_load: perf.domContentLoadedEventEnd - perf.responseEnd, // DOM loading timing\n      page_load: perf.loadEventStart, // Page load time\n\n      // Backend\n      ttfb: perf.responseStart - perf.requestStart,\n    }\n  }\n\n  /**\n   * Fetches all feature flags and experiments for the project.\n   * Results are cached for 5 minutes by default.\n   *\n   * @param options - Options for evaluating feature flags.\n   * @param forceRefresh - If true, bypasses the cache and fetches fresh data.\n   * @returns A promise that resolves to a record of flag keys to boolean values.\n   */\n  async getFeatureFlags(options?: FeatureFlagsOptions, forceRefresh?: boolean): Promise<Record<string, boolean>> {\n    if (!isInBrowser()) {\n      return {}\n    }\n\n    const requestedProfileId = options?.profileId ?? this.options?.profileId\n\n    // Check cache first - must match profileId and not be expired\n    if (!forceRefresh && this.cachedData) {\n      const now = Date.now()\n      const isSameProfile = this.cachedData.profileId === requestedProfileId\n      if (isSameProfile && now - this.cachedData.timestamp < DEFAULT_CACHE_DURATION) {\n        return this.cachedData.flags\n      }\n    }\n\n    try {\n      await this.fetchFlagsAndExperiments(options)\n      return this.cachedData?.flags || {}\n    } catch (error) {\n      console.warn('[Swetrix] Error fetching feature flags:', error)\n      return this.cachedData?.flags || {}\n    }\n  }\n\n  /**\n   * Internal method to fetch both feature flags and experiments from the API.\n   */\n  private async fetchFlagsAndExperiments(options?: FeatureFlagsOptions | ExperimentOptions): Promise<void> {\n    const apiBase = this.getApiBase()\n    const body: { pid: string; profileId?: string } = {\n      pid: this.projectID,\n    }\n\n    // Use profileId from options, or fall back to global profileId\n    const profileId = options?.profileId ?? this.options?.profileId\n    if (profileId) {\n      body.profileId = profileId\n    }\n\n    const response = await fetch(`${apiBase}/feature-flag/evaluate`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify(body),\n    })\n\n    if (!response.ok) {\n      console.warn('[Swetrix] Failed to fetch feature flags and experiments:', response.status)\n      return\n    }\n\n    const data = (await response.json()) as {\n      flags: Record<string, boolean>\n      experiments?: Record<string, string>\n    }\n\n    // Use profileId from options, or fall back to global profileId\n    const cachedProfileId = options?.profileId ?? this.options?.profileId\n\n    // Update cache with both flags and experiments\n    this.cachedData = {\n      flags: data.flags || {},\n      experiments: data.experiments || {},\n      timestamp: Date.now(),\n      profileId: cachedProfileId,\n    }\n  }\n\n  /**\n   * Gets the value of a single feature flag.\n   *\n   * @param key - The feature flag key.\n   * @param options - Options for evaluating the feature flag.\n   * @param defaultValue - Default value to return if the flag is not found. Defaults to false.\n   * @returns A promise that resolves to the boolean value of the flag.\n   */\n  async getFeatureFlag(key: string, options?: FeatureFlagsOptions, defaultValue: boolean = false): Promise<boolean> {\n    const flags = await this.getFeatureFlags(options)\n    return flags[key] ?? defaultValue\n  }\n\n  /**\n   * Clears the cached feature flags and experiments, forcing a fresh fetch on the next call.\n   */\n  clearFeatureFlagsCache(): void {\n    this.cachedData = null\n  }\n\n  /**\n   * Fetches all A/B test experiments for the project.\n   * Results are cached for 5 minutes by default (shared cache with feature flags).\n   *\n   * @param options - Options for evaluating experiments.\n   * @param forceRefresh - If true, bypasses the cache and fetches fresh data.\n   * @returns A promise that resolves to a record of experiment IDs to variant keys.\n   *\n   * @example\n   * ```typescript\n   * const experiments = await getExperiments()\n   * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }\n   * ```\n   */\n  async getExperiments(options?: ExperimentOptions, forceRefresh?: boolean): Promise<Record<string, string>> {\n    if (!isInBrowser()) {\n      return {}\n    }\n\n    const requestedProfileId = options?.profileId ?? this.options?.profileId\n\n    // Check cache first - must match profileId and not be expired\n    if (!forceRefresh && this.cachedData) {\n      const now = Date.now()\n      const isSameProfile = this.cachedData.profileId === requestedProfileId\n      if (isSameProfile && now - this.cachedData.timestamp < DEFAULT_CACHE_DURATION) {\n        return this.cachedData.experiments\n      }\n    }\n\n    try {\n      await this.fetchFlagsAndExperiments(options)\n      return this.cachedData?.experiments || {}\n    } catch (error) {\n      console.warn('[Swetrix] Error fetching experiments:', error)\n      return this.cachedData?.experiments || {}\n    }\n  }\n\n  /**\n   * Gets the variant key for a specific A/B test experiment.\n   *\n   * @param experimentId - The experiment ID.\n   * @param options - Options for evaluating the experiment.\n   * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.\n   * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.\n   *\n   * @example\n   * ```typescript\n   * const variant = await getExperiment('checkout-redesign')\n   *\n   * if (variant === 'new-checkout') {\n   *   // Show new checkout flow\n   * } else {\n   *   // Show control (original) checkout\n   * }\n   * ```\n   */\n  async getExperiment(\n    experimentId: string,\n    options?: ExperimentOptions,\n    defaultVariant: string | null = null,\n  ): Promise<string | null> {\n    const experiments = await this.getExperiments(options)\n    return experiments[experimentId] ?? defaultVariant\n  }\n\n  /**\n   * Clears the cached experiments (alias for clearFeatureFlagsCache since they share the same cache).\n   */\n  clearExperimentsCache(): void {\n    this.cachedData = null\n  }\n\n  /**\n   * Gets the anonymous profile ID for the current visitor.\n   * If profileId was set via init options, returns that.\n   * Otherwise, requests server to generate one from IP/UA hash.\n   *\n   * This ID can be used for revenue attribution with payment providers.\n   *\n   * @returns A promise that resolves to the profile ID string, or null on error.\n   *\n   * @example\n   * ```typescript\n   * const profileId = await swetrix.getProfileId()\n   *\n   * // Pass to Paddle Checkout for revenue attribution\n   * Paddle.Checkout.open({\n   *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n   *   customData: {\n   *     swetrix_profile_id: profileId,\n   *     swetrix_session_id: await swetrix.getSessionId()\n   *   }\n   * })\n   * ```\n   */\n  async getProfileId(): Promise<string | null> {\n    // If profileId is already set in options, return it\n    if (this.options?.profileId) {\n      return this.options.profileId\n    }\n\n    if (!isInBrowser()) {\n      return null\n    }\n\n    try {\n      const apiBase = this.getApiBase()\n      const response = await fetch(`${apiBase}/log/profile-id`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({ pid: this.projectID }),\n      })\n\n      if (!response.ok) {\n        return null\n      }\n\n      const data = (await response.json()) as { profileId: string | null }\n      return data.profileId\n    } catch {\n      return null\n    }\n  }\n\n  /**\n   * Gets the current session ID for the visitor.\n   * Session IDs are generated server-side based on IP and user agent.\n   *\n   * This ID can be used for revenue attribution with payment providers.\n   *\n   * @returns A promise that resolves to the session ID string, or null on error.\n   *\n   * @example\n   * ```typescript\n   * const sessionId = await swetrix.getSessionId()\n   *\n   * // Pass to Paddle Checkout for revenue attribution\n   * Paddle.Checkout.open({\n   *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n   *   customData: {\n   *     swetrix_profile_id: await swetrix.getProfileId(),\n   *     swetrix_session_id: sessionId\n   *   }\n   * })\n   * ```\n   */\n  async getSessionId(): Promise<string | null> {\n    if (!isInBrowser()) {\n      return null\n    }\n\n    try {\n      const apiBase = this.getApiBase()\n      const response = await fetch(`${apiBase}/log/session-id`, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({ pid: this.projectID }),\n      })\n\n      if (!response.ok) {\n        return null\n      }\n\n      const data = (await response.json()) as { sessionId: string | null }\n      return data.sessionId\n    } catch {\n      return null\n    }\n  }\n\n  /**\n   * Gets the API base URL (without /log suffix).\n   */\n  private getApiBase(): string {\n    if (this.options?.apiURL) {\n      // Remove trailing /log if present\n      return this.options.apiURL.replace(/\\/log\\/?$/, '')\n    }\n    return DEFAULT_API_BASE\n  }\n\n  private heartbeat(): void {\n    if (!this.pageViewsOptions?.heartbeatOnBackground && document.visibilityState === 'hidden') {\n      return\n    }\n\n    const data: { pid: string; profileId?: string } = {\n      pid: this.projectID,\n    }\n\n    if (this.options?.profileId) {\n      data.profileId = this.options.profileId\n    }\n\n    this.sendRequest('hb', data)\n  }\n\n  // Tracking path changes. If path changes -> calling this.trackPage method\n  private trackPathChange(): void {\n    if (!this.pageData) return\n    const newPath = getPath({\n      hash: this.pageViewsOptions?.hash,\n      search: this.pageViewsOptions?.search,\n    })\n    const { path } = this.pageData\n\n    if (path !== newPath) {\n      this.trackPage(newPath, false)\n    }\n  }\n\n  private trackPage(pg: string, unique: boolean = false): void {\n    if (!this.pageData) return\n    this.pageData.path = pg\n\n    const perf = this.getPerformanceStats()\n\n    this.activePage = pg\n    this.submitPageView({ pg }, unique, perf, true)\n  }\n\n  submitPageView(\n    payload: Partial<IPageViewPayload>,\n    unique: boolean,\n    perf: IPerfPayload | {},\n    evokeCallback?: boolean,\n  ): void {\n    const privateData = {\n      pid: this.projectID,\n      perf,\n      unique,\n    }\n\n    const pvPayload = {\n      lc: getLocale(),\n      tz: getTimezone(),\n      ref: getReferrer(),\n      so: getUTMSource(),\n      me: getUTMMedium(),\n      ca: getUTMCampaign(),\n      te: getUTMTerm(),\n      co: getUTMContent(),\n      profileId: this.options?.profileId,\n      ...payload,\n    }\n\n    if (evokeCallback && this.pageViewsOptions?.callback) {\n      const callbackResult = this.pageViewsOptions.callback(pvPayload as IPageViewPayload)\n\n      if (callbackResult === false) {\n        return\n      }\n\n      if (callbackResult && typeof callbackResult === 'object') {\n        Object.assign(pvPayload, callbackResult)\n      }\n    }\n\n    Object.assign(pvPayload, privateData)\n\n    this.sendRequest('', pvPayload)\n  }\n\n  private canTrack(): boolean {\n    if (\n      this.options?.disabled ||\n      !isInBrowser() ||\n      (this.options?.respectDNT && window.navigator?.doNotTrack === '1') ||\n      (!this.options?.devMode && isLocalhost()) ||\n      isAutomated()\n    ) {\n      return false\n    }\n\n    return true\n  }\n\n  private async sendRequest(path: string, body: object): Promise<void> {\n    const host = this.options?.apiURL || DEFAULT_API_HOST\n    await fetch(`${host}/${path}`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify(body),\n    })\n  }\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "import {\n  Lib,\n  LibOptions,\n  TrackEventOptions,\n  PageViewsOptions,\n  ErrorOptions,\n  PageActions,\n  ErrorActions,\n  defaultActions,\n  IErrorEventPayload,\n  IPageViewPayload,\n  FeatureFlagsOptions,\n  ExperimentOptions,\n} from './Lib.js'\n\nexport let LIB_INSTANCE: Lib | null = null\n\n/**\n * Initialise the tracking library instance (other methods won't work if the library is not initialised).\n *\n * @param {string} pid The Project ID to link the instance of Swetrix.js to.\n * @param {LibOptions} options Options related to the tracking.\n * @returns {Lib} Instance of the Swetrix.js.\n */\nexport function init(pid: string, options?: LibOptions): Lib {\n  if (!LIB_INSTANCE) {\n    LIB_INSTANCE = new Lib(pid, options)\n  }\n\n  return LIB_INSTANCE\n}\n\n/**\n * With this function you are able to track any custom events you want.\n * You should never send any identifiable data (like User ID, email, session cookie, etc.) as an event name.\n * The total number of track calls and their conversion rate will be saved.\n *\n * @param {TrackEventOptions} event The options related to the custom event.\n */\nexport async function track(event: TrackEventOptions): Promise<void> {\n  if (!LIB_INSTANCE) return\n\n  await LIB_INSTANCE.track(event)\n}\n\n/**\n * With this function you are able to automatically track pageviews across your application.\n *\n * @param {PageViewsOptions} options Pageviews tracking options.\n * @returns {PageActions} The actions related to the tracking. Used to stop tracking pages.\n */\nexport function trackViews(options?: PageViewsOptions): Promise<PageActions> {\n  return new Promise((resolve) => {\n    if (!LIB_INSTANCE) {\n      resolve(defaultActions)\n      return\n    }\n\n    // We need to verify that document.readyState is complete for the performance stats to be collected correctly.\n    if (typeof document === 'undefined' || document.readyState === 'complete') {\n      resolve(LIB_INSTANCE.trackPageViews(options))\n    } else {\n      window.addEventListener('load', () => {\n        // @ts-ignore\n        resolve(LIB_INSTANCE.trackPageViews(options))\n      })\n    }\n  })\n}\n\n/**\n * This function is used to set up automatic error events tracking.\n * It set's up an error listener, and whenever an error happens, it gets tracked.\n *\n * @returns {ErrorActions} The actions related to the tracking. Used to stop tracking errors.\n */\nexport function trackErrors(options?: ErrorOptions): ErrorActions {\n  if (!LIB_INSTANCE) {\n    return defaultActions\n  }\n\n  return LIB_INSTANCE.trackErrors(options)\n}\n\n/**\n * This function is used to manually track an error event.\n * It's useful if you want to track specific errors in your application.\n *\n * @param payload Swetrix error object to send.\n * @returns void\n */\nexport function trackError(payload: IErrorEventPayload): void {\n  if (!LIB_INSTANCE) return\n\n  LIB_INSTANCE.submitError(payload, false)\n}\n\n/**\n * This function is used to manually track a page view event.\n * It's useful if your application uses esoteric routing which is not supported by Swetrix by default.\n *\n * @deprecated This function is deprecated and will be removed soon, please use the `pageview` instead.\n * @param pg Path of the page to track (this will be sent to the Swetrix API and displayed in the dashboard).\n * @param _prev Path of the previous page (deprecated and ignored).\n * @param unique If set to `true`, only 1 event with the same ID will be saved per user session.\n * @returns void\n */\nexport function trackPageview(pg: string, _prev?: string, unique?: boolean): void {\n  if (!LIB_INSTANCE) return\n\n  LIB_INSTANCE.submitPageView({ pg }, Boolean(unique), {})\n}\n\nexport interface IPageviewOptions {\n  payload: Partial<IPageViewPayload>\n  unique?: boolean\n}\n\nexport function pageview(options: IPageviewOptions): void {\n  if (!LIB_INSTANCE) return\n\n  LIB_INSTANCE.submitPageView(options.payload, Boolean(options.unique), {})\n}\n\n/**\n * Fetches all feature flags for the project.\n * Results are cached for 5 minutes by default.\n *\n * @param options - Options for evaluating feature flags (visitorId, attributes).\n * @param forceRefresh - If true, bypasses the cache and fetches fresh flags.\n * @returns A promise that resolves to a record of flag keys to boolean values.\n *\n * @example\n * ```typescript\n * const flags = await getFeatureFlags({\n *   visitorId: 'user-123',\n *   attributes: { cc: 'US', dv: 'desktop' }\n * })\n *\n * if (flags['new-checkout']) {\n *   // Show new checkout flow\n * }\n * ```\n */\nexport async function getFeatureFlags(\n  options?: FeatureFlagsOptions,\n  forceRefresh?: boolean,\n): Promise<Record<string, boolean>> {\n  if (!LIB_INSTANCE) return {}\n\n  return LIB_INSTANCE.getFeatureFlags(options, forceRefresh)\n}\n\n/**\n * Gets the value of a single feature flag.\n *\n * @param key - The feature flag key.\n * @param options - Options for evaluating the feature flag (visitorId, attributes).\n * @param defaultValue - Default value to return if the flag is not found. Defaults to false.\n * @returns A promise that resolves to the boolean value of the flag.\n *\n * @example\n * ```typescript\n * const isEnabled = await getFeatureFlag('dark-mode', { visitorId: 'user-123' })\n *\n * if (isEnabled) {\n *   // Enable dark mode\n * }\n * ```\n */\nexport async function getFeatureFlag(\n  key: string,\n  options?: FeatureFlagsOptions,\n  defaultValue: boolean = false,\n): Promise<boolean> {\n  if (!LIB_INSTANCE) return defaultValue\n\n  return LIB_INSTANCE.getFeatureFlag(key, options, defaultValue)\n}\n\n/**\n * Clears the cached feature flags, forcing a fresh fetch on the next call.\n * Useful when you know the user's context has changed significantly.\n */\nexport function clearFeatureFlagsCache(): void {\n  if (!LIB_INSTANCE) return\n\n  LIB_INSTANCE.clearFeatureFlagsCache()\n}\n\n/**\n * Fetches all A/B test experiments for the project.\n * Results are cached for 5 minutes by default (shared cache with feature flags).\n *\n * @param options - Options for evaluating experiments.\n * @param forceRefresh - If true, bypasses the cache and fetches fresh data.\n * @returns A promise that resolves to a record of experiment IDs to variant keys.\n *\n * @example\n * ```typescript\n * const experiments = await getExperiments()\n * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }\n *\n * // Use the assigned variant\n * const checkoutVariant = experiments['checkout-experiment-id']\n * if (checkoutVariant === 'new-checkout') {\n *   showNewCheckout()\n * } else {\n *   showOriginalCheckout()\n * }\n * ```\n */\nexport async function getExperiments(\n  options?: ExperimentOptions,\n  forceRefresh?: boolean,\n): Promise<Record<string, string>> {\n  if (!LIB_INSTANCE) return {}\n\n  return LIB_INSTANCE.getExperiments(options, forceRefresh)\n}\n\n/**\n * Gets the variant key for a specific A/B test experiment.\n *\n * @param experimentId - The experiment ID.\n * @param options - Options for evaluating the experiment.\n * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.\n * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.\n *\n * @example\n * ```typescript\n * const variant = await getExperiment('checkout-redesign-experiment-id')\n *\n * if (variant === 'new-checkout') {\n *   // Show new checkout flow\n *   showNewCheckout()\n * } else if (variant === 'control') {\n *   // Show original checkout (control group)\n *   showOriginalCheckout()\n * } else {\n *   // Experiment not running or user not included\n *   showOriginalCheckout()\n * }\n * ```\n */\nexport async function getExperiment(\n  experimentId: string,\n  options?: ExperimentOptions,\n  defaultVariant: string | null = null,\n): Promise<string | null> {\n  if (!LIB_INSTANCE) return defaultVariant\n\n  return LIB_INSTANCE.getExperiment(experimentId, options, defaultVariant)\n}\n\n/**\n * Clears the cached experiments, forcing a fresh fetch on the next call.\n * This is an alias for clearFeatureFlagsCache since experiments and flags share the same cache.\n */\nexport function clearExperimentsCache(): void {\n  if (!LIB_INSTANCE) return\n\n  LIB_INSTANCE.clearExperimentsCache()\n}\n\n/**\n * Gets the anonymous profile ID for the current visitor.\n * If profileId was set via init options, returns that.\n * Otherwise, requests server to generate one from IP/UA hash.\n *\n * This ID can be used for revenue attribution with payment providers like Paddle.\n *\n * @returns A promise that resolves to the profile ID string, or null on error.\n *\n * @example\n * ```typescript\n * const profileId = await getProfileId()\n *\n * // Pass to Paddle Checkout for revenue attribution\n * Paddle.Checkout.open({\n *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n *   customData: {\n *     swetrix_profile_id: profileId,\n *     swetrix_session_id: await getSessionId()\n *   }\n * })\n * ```\n */\nexport async function getProfileId(): Promise<string | null> {\n  if (!LIB_INSTANCE) return null\n\n  return LIB_INSTANCE.getProfileId()\n}\n\n/**\n * Gets the current session ID for the visitor.\n * Session IDs are generated server-side based on IP and user agent.\n *\n * This ID can be used for revenue attribution with payment providers like Paddle.\n *\n * @returns A promise that resolves to the session ID string, or null on error.\n *\n * @example\n * ```typescript\n * const sessionId = await getSessionId()\n *\n * // Pass to Paddle Checkout for revenue attribution\n * Paddle.Checkout.open({\n *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],\n *   customData: {\n *     swetrix_profile_id: await getProfileId(),\n *     swetrix_session_id: sessionId\n *   }\n * })\n * ```\n */\nexport async function getSessionId(): Promise<string | null> {\n  if (!LIB_INSTANCE) return null\n\n  return LIB_INSTANCE.getSessionId()\n}\n\nexport {\n  LibOptions,\n  TrackEventOptions,\n  PageViewsOptions,\n  ErrorOptions,\n  PageActions,\n  ErrorActions,\n  IErrorEventPayload,\n  IPageViewPayload,\n  FeatureFlagsOptions,\n  ExperimentOptions,\n}\n"
  },
  {
    "path": "src/utils.ts",
    "content": "interface IGetPath {\n  hash?: boolean\n  search?: boolean\n}\n\nconst findInSearch = (exp: RegExp): string | undefined => {\n  const res = location.search.match(exp)\n  return (res && res[2]) || undefined\n}\n\nconst utmSourceRegex = /[?&](ref|source|utm_source|gad_source)=([^?&]+)/\nconst utmCampaignRegex = /[?&](utm_campaign|gad_campaignid)=([^?&]+)/\nconst utmMediumRegex = /[?&](utm_medium)=([^?&]+)/\nconst utmTermRegex = /[?&](utm_term)=([^?&]+)/\nconst utmContentRegex = /[?&](utm_content)=([^?&]+)/\n\nconst gclidRegex = /[?&](gclid)=([^?&]+)/\n\nconst getGclid = () => {\n  return findInSearch(gclidRegex) ? '<gclid>' : undefined\n}\n\nexport const isInBrowser = () => {\n  return typeof window !== 'undefined'\n}\n\nexport const isLocalhost = () => {\n  return location?.hostname === 'localhost' || location?.hostname === '127.0.0.1' || location?.hostname === ''\n}\n\nexport const isAutomated = () => {\n  return navigator?.webdriver\n}\n\nexport const getLocale = () => {\n  return typeof navigator.languages !== 'undefined' ? navigator.languages[0] : navigator.language\n}\n\nexport const getTimezone = () => {\n  try {\n    return Intl.DateTimeFormat().resolvedOptions().timeZone\n  } catch (e) {\n    return\n  }\n}\n\nexport const getReferrer = (): string | undefined => {\n  return document.referrer || undefined\n}\n\nexport const getUTMSource = () => findInSearch(utmSourceRegex)\n\nexport const getUTMMedium = () => findInSearch(utmMediumRegex) || getGclid()\n\nexport const getUTMCampaign = () => findInSearch(utmCampaignRegex)\n\nexport const getUTMTerm = () => findInSearch(utmTermRegex)\n\nexport const getUTMContent = () => findInSearch(utmContentRegex)\n\n/**\n * Function used to track the current page (path) of the application.\n * Will work in cases where the path looks like:\n * - /path\n * - /#/path\n * - /path?search\n * - /path?search#hash\n * - /path#hash?search\n *\n * @param options - Options for the function.\n * @param options.hash - Whether to trigger on hash change.\n * @param options.search - Whether to trigger on search change.\n * @returns The path of the current page.\n */\nexport const getPath = (options: IGetPath): string => {\n  let result = location.pathname || ''\n\n  if (options.hash) {\n    const hashIndex = location.hash.indexOf('?')\n    const hashString = hashIndex > -1 ? location.hash.substring(0, hashIndex) : location.hash\n    result += hashString\n  }\n\n  if (options.search) {\n    const hashIndex = location.hash.indexOf('?')\n    const searchString = location.search || (hashIndex > -1 ? location.hash.substring(hashIndex) : '')\n    result += searchString\n  }\n\n  return result\n}\n"
  },
  {
    "path": "tests/README.md",
    "content": "# Swetrix JS Tests\n\nThis directory contains tests for the Swetrix JavaScript analytics client.\n\n## Test Structure\n\n- **initialisation.test.ts**: Tests for library initialisation and core functionality\n- **pageview.test.ts**: Tests for page view tracking functionality\n- **events.test.ts**: Tests for custom event tracking\n- **errors.test.ts**: Tests for error tracking\n- **utils.test.ts**: Tests for utility functions (utils.ts file)\n\n## Running Tests\n\nTo run the tests, use:\n\n```bash\nnpm test\n```\n\nFor watching mode:\n\n```bash\nnpm run test:watch\n```\n\n## Test Environment\n\nTests use Jest with jsdom environment to simulate a browser environment. The library is tested in isolation with mocked API requests.\n\n## Mocking Strategy\n\n- API requests are mocked to avoid actual network requests\n- Browser APIs (window, document, navigator) are mocked as needed\n- The library's internal methods are selectively mocked or spied on to verify behaviour\n\n## Writing New Tests\n\nWhen adding new tests:\n\n1. Follow the existing patterns of mocking and setup\n2. Use descriptive test names that explain what aspect is being tested\n3. Structure tests with Arrange-Act-Assert pattern\n4. Clean up mocks between tests using beforeEach/afterEach\n\n## Coverage\n\nTest coverage can be checked by running:\n\n```bash\nnpm test -- --coverage\n```\n"
  },
  {
    "path": "tests/errors.test.ts",
    "content": "import { init, trackError, trackErrors } from '../src/index'\nimport { Lib } from '../src/Lib'\n\njest.mock('../src/Lib', () => {\n  const originalModule = jest.requireActual('../src/Lib')\n\n  return {\n    ...originalModule,\n    Lib: class MockLib extends originalModule.Lib {\n      sendRequest = jest.fn().mockResolvedValue(undefined)\n\n      captureError = jest.fn().mockImplementation(function (this: any, event: ErrorEvent) {\n        const errorPayload = {\n          name: event.error?.name || 'Error',\n          message: event.message,\n          filename: event.filename,\n          lineno: event.lineno,\n          colno: event.colno,\n          stackTrace: event.error?.stack,\n        }\n\n        this.submitError(errorPayload, true)\n      })\n\n      submitError = jest.fn().mockImplementation(function (this: any, payload: any, evokeCallback: boolean = true) {\n        const formattedPayload = {\n          pid: this.projectID,\n          name: payload.name,\n          message: payload.message,\n          filename: payload.filename,\n          lineno: payload.lineno,\n          colno: payload.colno,\n          stackTrace: payload.stackTrace,\n          meta: payload.meta,\n          pg: '/test-page',\n          lc: 'en-US',\n          tz: 'Europe/London',\n        }\n\n        // Simulate callback behavior if evokeCallback is true and callback exists\n        if (evokeCallback && this.errorsOptions?.callback) {\n          const callbackResult = this.errorsOptions.callback(formattedPayload)\n\n          if (callbackResult === false) {\n            return\n          }\n\n          if (callbackResult && typeof callbackResult === 'object') {\n            Object.assign(formattedPayload, callbackResult)\n          }\n        }\n\n        this.sendRequest('error', formattedPayload)\n      })\n\n      trackErrors(options?: any) {\n        return {\n          stop: () => {},\n        }\n      }\n    },\n  }\n})\n\ndescribe('Error Tracking', () => {\n  const PROJECT_ID = 'test-project-id'\n  let libInstance: Lib\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n\n    libInstance = init(PROJECT_ID, { devMode: true }) as Lib\n\n    Object.defineProperty(window, 'location', {\n      value: {\n        hostname: 'example.com',\n        pathname: '/test-page',\n        hash: '',\n        search: '',\n      },\n      writable: true,\n    })\n\n    window.addEventListener = jest.fn()\n    window.removeEventListener = jest.fn()\n  })\n\n  test('trackError function should track an error event', () => {\n    // Arrange\n    const errorPayload = {\n      name: 'TypeError',\n      message: 'Cannot read property of undefined',\n      filename: 'app.js',\n      lineno: 42,\n      colno: 10,\n    }\n\n    // Act\n    trackError(errorPayload)\n\n    // Assert\n    expect((libInstance as any).sendRequest).toHaveBeenCalledTimes(1)\n    // We've changed our expectation to match the actual format used by the library\n    expect((libInstance as any).sendRequest).toHaveBeenCalledWith(\n      'error',\n      expect.objectContaining({\n        pid: PROJECT_ID,\n        name: errorPayload.name,\n        message: errorPayload.message,\n        filename: errorPayload.filename,\n        lineno: errorPayload.lineno,\n        colno: errorPayload.colno,\n      }),\n    )\n  })\n\n  test('trackErrors function should return actions object', () => {\n    // Mock the trackErrors method\n    const trackErrorsSpy = jest.spyOn(libInstance, 'trackErrors')\n\n    // Act\n    const actions = trackErrors()\n\n    // Assert\n    expect(trackErrorsSpy).toHaveBeenCalled()\n    expect(actions).toHaveProperty('stop')\n    expect(typeof actions.stop).toBe('function')\n  })\n\n  test('trackErrors with sample rate should pass the sampleRate option', () => {\n    // Mock the trackErrors method\n    const trackErrorsSpy = jest.spyOn(libInstance, 'trackErrors')\n\n    // Act\n    trackErrors({ sampleRate: 0.5 })\n\n    // Assert\n    expect(trackErrorsSpy).toHaveBeenCalledWith({ sampleRate: 0.5 })\n  })\n\n  test('trackErrors with callback should pass the callback option', () => {\n    // Set up a callback\n    const callbackFn = jest.fn().mockReturnValue({\n      name: 'CustomError',\n      message: 'Modified by callback',\n    })\n\n    // Mock the trackErrors method\n    const trackErrorsSpy = jest.spyOn(libInstance, 'trackErrors')\n\n    // Act\n    trackErrors({ callback: callbackFn })\n\n    // Assert\n    expect(trackErrorsSpy).toHaveBeenCalledWith({ callback: callbackFn })\n  })\n\n  test('trackErrors stop function should call the returned stop function', () => {\n    // Arrange\n    const mockStop = jest.fn()\n    jest.spyOn(libInstance, 'trackErrors').mockReturnValue({ stop: mockStop })\n\n    // Act\n    const actions = trackErrors()\n    actions.stop()\n\n    // Assert\n    expect(mockStop).toHaveBeenCalled()\n  })\n\n  test('trackError should handle meta property', () => {\n    // Arrange\n    const errorPayload = {\n      name: 'ValidationError',\n      message: 'Invalid input provided',\n      filename: 'validation.js',\n      lineno: 15,\n      colno: 5,\n      meta: {\n        userId: 'user123',\n        feature: 'login',\n        environment: 'production',\n      },\n    }\n\n    // Act\n    trackError(errorPayload)\n\n    // Assert\n    expect((libInstance as any).sendRequest).toHaveBeenCalledWith(\n      'error',\n      expect.objectContaining({\n        pid: PROJECT_ID,\n        name: errorPayload.name,\n        message: errorPayload.message,\n        filename: errorPayload.filename,\n        lineno: errorPayload.lineno,\n        colno: errorPayload.colno,\n        meta: errorPayload.meta,\n      }),\n    )\n  })\n\n  test('captureError should automatically extract stackTrace from ErrorEvent', () => {\n    // Arrange\n    const mockError = new Error('Test error message')\n    mockError.stack = 'Error: Test error message\\n    at test.js:10:5\\n    at Object.run (test.js:5:2)'\n\n    const errorEvent = new ErrorEvent('error', {\n      error: mockError,\n      message: 'Test error message',\n      filename: 'test.js',\n      lineno: 10,\n      colno: 5,\n    })\n\n    // Act\n    ;(libInstance as any).captureError(errorEvent)\n\n    // Assert\n    expect((libInstance as any).submitError).toHaveBeenCalledWith(\n      expect.objectContaining({\n        name: 'Error',\n        message: 'Test error message',\n        filename: 'test.js',\n        lineno: 10,\n        colno: 5,\n        stackTrace: mockError.stack,\n      }),\n      true,\n    )\n  })\n})\n"
  },
  {
    "path": "tests/events.test.ts",
    "content": "import { init, track } from '../src/index'\nimport { Lib } from '../src/Lib'\n\njest.mock('../src/Lib', () => {\n  const originalModule = jest.requireActual('../src/Lib')\n\n  return {\n    ...originalModule,\n    Lib: class MockLib extends originalModule.Lib {\n      sendRequest = jest.fn().mockResolvedValue(undefined)\n    },\n  }\n})\n\ndescribe('Custom Event Tracking', () => {\n  const PROJECT_ID = 'test-project-id'\n  let libInstance: Lib\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n\n    libInstance = init(PROJECT_ID, { devMode: true }) as Lib\n\n    Object.defineProperty(window, 'location', {\n      value: {\n        hostname: 'example.com',\n        pathname: '/test-page',\n        hash: '',\n        search: '',\n      },\n      writable: true,\n    })\n  })\n\n  test('track function should track a custom event', async () => {\n    // Arrange\n    const eventName = 'button_click'\n\n    // Act\n    await track({ ev: eventName })\n\n    // Assert\n    expect((libInstance as any).sendRequest).toHaveBeenCalledTimes(1)\n    expect((libInstance as any).sendRequest).toHaveBeenCalledWith(\n      expect.any(String),\n      expect.objectContaining({\n        pid: PROJECT_ID,\n        ev: eventName,\n      }),\n    )\n  })\n\n  test('track function with unique flag should track unique event', async () => {\n    // Arrange\n    const eventName = 'form_submit'\n\n    // Act\n    await track({ ev: eventName, unique: true })\n\n    // Assert\n    expect((libInstance as any).sendRequest).toHaveBeenCalledTimes(1)\n    expect((libInstance as any).sendRequest).toHaveBeenCalledWith(\n      expect.any(String),\n      expect.objectContaining({\n        pid: PROJECT_ID,\n        ev: eventName,\n        unique: true,\n      }),\n    )\n  })\n\n  test('track function with metadata should include metadata in request', async () => {\n    // Arrange\n    const eventName = 'purchase'\n    const metadata = {\n      product: 'premium_plan',\n      price: '99.99',\n    }\n\n    // Act\n    await track({\n      ev: eventName,\n      meta: metadata,\n    })\n\n    // Assert\n    expect((libInstance as any).sendRequest).toHaveBeenCalledTimes(1)\n    expect((libInstance as any).sendRequest).toHaveBeenCalledWith(\n      expect.any(String),\n      expect.objectContaining({\n        pid: PROJECT_ID,\n        ev: eventName,\n        meta: metadata,\n      }),\n    )\n  })\n\n  test('should not track when library is not initialized', async () => {\n    // Create a new module import to reset the LIB_INSTANCE\n    jest.resetModules()\n    const { track: newTrack } = await import('../src/index')\n\n    // Act\n    await newTrack({ ev: 'test_event' })\n\n    // Assert - no request should be sent\n    expect((libInstance as any).sendRequest).not.toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "tests/experiments.test.ts",
    "content": "import { init, getExperiment, getExperiments, clearExperimentsCache } from '../src/index'\nimport { Lib } from '../src/Lib'\n\n// Mock fetch globally\nconst mockFetch = jest.fn()\nglobal.fetch = mockFetch\n\ndescribe('A/B Testing Experiments', () => {\n  const PROJECT_ID = 'test-project-id'\n  let libInstance: Lib\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n    jest.resetModules()\n\n    // Reset fetch mock\n    mockFetch.mockReset()\n\n    Object.defineProperty(window, 'location', {\n      value: {\n        hostname: 'example.com',\n        pathname: '/test-page',\n        hash: '',\n        search: '',\n      },\n      writable: true,\n    })\n  })\n\n  describe('getExperiments', () => {\n    beforeEach(async () => {\n      jest.resetModules()\n      const { init: freshInit } = await import('../src/index')\n      libInstance = freshInit(PROJECT_ID, { devMode: true }) as Lib\n    })\n\n    test('should return experiments from API response', async () => {\n      // Arrange\n      mockFetch.mockResolvedValueOnce({\n        ok: true,\n        json: async () => ({\n          flags: { 'feature-flag-1': true },\n          experiments: {\n            'exp-checkout': 'variant-b',\n            'exp-pricing': 'control',\n          },\n        }),\n      })\n\n      // Act\n      const { getExperiments: freshGetExperiments } = await import('../src/index')\n      const experiments = await freshGetExperiments()\n\n      // Assert\n      expect(experiments).toEqual({\n        'exp-checkout': 'variant-b',\n        'exp-pricing': 'control',\n      })\n    })\n\n    test('should return empty object when no experiments in response', async () => {\n      // Arrange\n      mockFetch.mockResolvedValueOnce({\n        ok: true,\n        json: async () => ({\n          flags: { 'feature-flag-1': true },\n        }),\n      })\n\n      // Act\n      const { getExperiments: freshGetExperiments } = await import('../src/index')\n      const experiments = await freshGetExperiments()\n\n      // Assert\n      expect(experiments).toEqual({})\n    })\n\n    test('should use cached experiments on subsequent calls', async () => {\n      // Arrange\n      mockFetch.mockResolvedValueOnce({\n        ok: true,\n        json: async () => ({\n          flags: {},\n          experiments: { 'exp-1': 'variant-a' },\n        }),\n      })\n\n      // Act\n      const { getExperiments: freshGetExperiments } = await import('../src/index')\n      await freshGetExperiments()\n      await freshGetExperiments()\n\n      // Assert - fetch should only be called once\n      expect(mockFetch).toHaveBeenCalledTimes(1)\n    })\n\n    test('should bypass cache when forceRefresh is true', async () => {\n      // Arrange\n      mockFetch\n        .mockResolvedValueOnce({\n          ok: true,\n          json: async () => ({\n            flags: {},\n            experiments: { 'exp-1': 'variant-a' },\n          }),\n        })\n        .mockResolvedValueOnce({\n          ok: true,\n          json: async () => ({\n            flags: {},\n            experiments: { 'exp-1': 'variant-b' },\n          }),\n        })\n\n      // Act\n      const { getExperiments: freshGetExperiments } = await import('../src/index')\n      const first = await freshGetExperiments()\n      const second = await freshGetExperiments(undefined, true)\n\n      // Assert\n      expect(mockFetch).toHaveBeenCalledTimes(2)\n      expect(first).toEqual({ 'exp-1': 'variant-a' })\n      expect(second).toEqual({ 'exp-1': 'variant-b' })\n    })\n  })\n\n  describe('getExperiment', () => {\n    beforeEach(async () => {\n      jest.resetModules()\n      const { init: freshInit } = await import('../src/index')\n      libInstance = freshInit(PROJECT_ID, { devMode: true }) as Lib\n    })\n\n    test('should return specific experiment variant', async () => {\n      // Arrange\n      mockFetch.mockResolvedValueOnce({\n        ok: true,\n        json: async () => ({\n          flags: {},\n          experiments: {\n            'exp-checkout': 'new-checkout',\n            'exp-pricing': 'control',\n          },\n        }),\n      })\n\n      // Act\n      const { getExperiment: freshGetExperiment } = await import('../src/index')\n      const variant = await freshGetExperiment('exp-checkout')\n\n      // Assert\n      expect(variant).toBe('new-checkout')\n    })\n\n    test('should return defaultVariant when experiment not found', async () => {\n      // Arrange\n      mockFetch.mockResolvedValueOnce({\n        ok: true,\n        json: async () => ({\n          flags: {},\n          experiments: {},\n        }),\n      })\n\n      // Act\n      const { getExperiment: freshGetExperiment } = await import('../src/index')\n      const variant = await freshGetExperiment('non-existent', undefined, 'fallback')\n\n      // Assert\n      expect(variant).toBe('fallback')\n    })\n\n    test('should return null when experiment not found and no default provided', async () => {\n      // Arrange\n      mockFetch.mockResolvedValueOnce({\n        ok: true,\n        json: async () => ({\n          flags: {},\n          experiments: {},\n        }),\n      })\n\n      // Act\n      const { getExperiment: freshGetExperiment } = await import('../src/index')\n      const variant = await freshGetExperiment('non-existent')\n\n      // Assert\n      expect(variant).toBeNull()\n    })\n  })\n\n  describe('clearExperimentsCache', () => {\n    test('should clear cache and fetch fresh data on next call', async () => {\n      jest.resetModules()\n      const {\n        init: freshInit,\n        getExperiments: freshGetExperiments,\n        clearExperimentsCache: freshClearCache,\n      } = await import('../src/index')\n      freshInit(PROJECT_ID, { devMode: true })\n\n      // Arrange\n      mockFetch\n        .mockResolvedValueOnce({\n          ok: true,\n          json: async () => ({\n            flags: {},\n            experiments: { 'exp-1': 'variant-a' },\n          }),\n        })\n        .mockResolvedValueOnce({\n          ok: true,\n          json: async () => ({\n            flags: {},\n            experiments: { 'exp-1': 'variant-b' },\n          }),\n        })\n\n      // Act\n      await freshGetExperiments()\n      freshClearCache()\n      const newExperiments = await freshGetExperiments()\n\n      // Assert\n      expect(mockFetch).toHaveBeenCalledTimes(2)\n      expect(newExperiments).toEqual({ 'exp-1': 'variant-b' })\n    })\n  })\n\n  describe('profileId handling', () => {\n    test('should include profileId in request when provided in options', async () => {\n      jest.resetModules()\n      const { init: freshInit, getExperiments: freshGetExperiments } = await import('../src/index')\n      freshInit(PROJECT_ID, { devMode: true })\n\n      mockFetch.mockResolvedValueOnce({\n        ok: true,\n        json: async () => ({\n          flags: {},\n          experiments: {},\n        }),\n      })\n\n      // Act\n      await freshGetExperiments({ profileId: 'user-123' })\n\n      // Assert\n      expect(mockFetch).toHaveBeenCalledWith(\n        expect.any(String),\n        expect.objectContaining({\n          body: JSON.stringify({\n            pid: PROJECT_ID,\n            profileId: 'user-123',\n          }),\n        }),\n      )\n    })\n\n    test('should use global profileId when not provided in options', async () => {\n      jest.resetModules()\n      const { init: freshInit, getExperiments: freshGetExperiments } = await import('../src/index')\n      freshInit(PROJECT_ID, { devMode: true, profileId: 'global-user' })\n\n      mockFetch.mockResolvedValueOnce({\n        ok: true,\n        json: async () => ({\n          flags: {},\n          experiments: {},\n        }),\n      })\n\n      // Act\n      await freshGetExperiments()\n\n      // Assert\n      expect(mockFetch).toHaveBeenCalledWith(\n        expect.any(String),\n        expect.objectContaining({\n          body: JSON.stringify({\n            pid: PROJECT_ID,\n            profileId: 'global-user',\n          }),\n        }),\n      )\n    })\n  })\n\n  describe('error handling', () => {\n    test('should return empty object when fetch fails', async () => {\n      jest.resetModules()\n      const { init: freshInit, getExperiments: freshGetExperiments } = await import('../src/index')\n      freshInit(PROJECT_ID, { devMode: true })\n\n      // Arrange\n      mockFetch.mockRejectedValueOnce(new Error('Network error'))\n\n      // Suppress console.warn for this test\n      const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()\n\n      // Act\n      const experiments = await freshGetExperiments()\n\n      // Assert\n      expect(experiments).toEqual({})\n      consoleSpy.mockRestore()\n    })\n\n    test('should return empty object when response is not ok', async () => {\n      jest.resetModules()\n      const { init: freshInit, getExperiments: freshGetExperiments } = await import('../src/index')\n      freshInit(PROJECT_ID, { devMode: true })\n\n      // Arrange\n      mockFetch.mockResolvedValueOnce({\n        ok: false,\n        status: 500,\n      })\n\n      // Suppress console.warn for this test\n      const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()\n\n      // Act\n      const experiments = await freshGetExperiments()\n\n      // Assert\n      expect(experiments).toEqual({})\n      consoleSpy.mockRestore()\n    })\n  })\n})\n"
  },
  {
    "path": "tests/initialisation.test.ts",
    "content": "import { init } from '../src/index'\nimport { Lib } from '../src/Lib'\n\njest.mock('../src/Lib', () => {\n  const originalModule = jest.requireActual('../src/Lib')\n\n  return {\n    ...originalModule,\n    Lib: class MockLib extends originalModule.Lib {\n      sendRequest = jest.fn().mockResolvedValue(undefined)\n      canTrack = jest.fn().mockReturnValue(true)\n    },\n  }\n})\n\ndescribe('Library Initialisation', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n\n    jest.resetModules()\n\n    Object.defineProperty(window, 'location', {\n      value: {\n        hostname: 'example.com',\n        pathname: '/test-page',\n        hash: '',\n        search: '',\n      },\n      writable: true,\n    })\n\n    Object.defineProperty(navigator, 'doNotTrack', {\n      value: null,\n      writable: true,\n    })\n  })\n\n  test('init should return a Lib instance', () => {\n    // Act\n    const instance = init('test-project-id')\n\n    // Assert\n    expect(instance).toBeInstanceOf(Lib)\n  })\n\n  test('init with devMode should work on localhost', () => {\n    // Arrange\n    Object.defineProperty(window, 'location', {\n      value: {\n        hostname: 'localhost',\n        pathname: '/',\n        hash: '',\n        search: '',\n      },\n      writable: true,\n    })\n\n    // Act\n    const instance = init('test-project-id', { devMode: true })\n\n    // Assert - no error should be thrown\n    expect(instance).toBeInstanceOf(Lib)\n    expect((instance as any).canTrack()).toBe(true)\n  })\n\n  test('init should respect DNT when respectDNT option is true', () => {\n    // Arrange\n    Object.defineProperty(navigator, 'doNotTrack', {\n      value: '1',\n      writable: true,\n    })\n\n    // Act\n    const instance = init('test-project-id', { respectDNT: true })\n\n    // Mock the canTrack method to return false for this instance\n    ;(instance as any).canTrack.mockReturnValue(false)\n\n    // Assert\n    expect((instance as any).canTrack()).toBe(false)\n  })\n\n  test('init should return the same instance when called multiple times', () => {\n    // Act\n    const instance1 = init('test-project-id')\n    const instance2 = init('another-id') // This should be ignored and return the first instance\n\n    // Assert\n    expect(instance1).toBe(instance2)\n  })\n})\n"
  },
  {
    "path": "tests/pageview.test.ts",
    "content": "import { init, pageview, trackViews } from '../src/index'\nimport { Lib } from '../src/Lib'\n\njest.mock('../src/Lib', () => {\n  const originalModule = jest.requireActual('../src/Lib')\n\n  return {\n    ...originalModule,\n    Lib: class MockLib extends originalModule.Lib {\n      sendRequest = jest.fn().mockResolvedValue(undefined)\n    },\n  }\n})\n\ndescribe('Pageview Tracking', () => {\n  const PROJECT_ID = 'test-project-id'\n  let libInstance: Lib\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n\n    libInstance = init(PROJECT_ID, { devMode: true }) as Lib\n\n    Object.defineProperty(window, 'location', {\n      value: {\n        hostname: 'example.com',\n        pathname: '/test-page',\n        hash: '',\n        search: '',\n      },\n      writable: true,\n    })\n\n    Object.defineProperty(document, 'referrer', {\n      value: 'https://google.com',\n      writable: true,\n    })\n  })\n\n  test('pageview function should track a page view', async () => {\n    // Arrange\n    const path = '/test-page'\n\n    // Act\n    pageview({\n      payload: { pg: path },\n      unique: false,\n    })\n\n    // Assert\n    expect((libInstance as any).sendRequest).toHaveBeenCalledTimes(1)\n    expect((libInstance as any).sendRequest).toHaveBeenCalledWith(\n      expect.any(String),\n      expect.objectContaining({\n        pid: PROJECT_ID,\n        pg: path,\n      }),\n    )\n  })\n\n  test('pageview function with unique flag should track unique page view', async () => {\n    // Arrange\n    const path = '/test-page'\n\n    // Act\n    pageview({\n      payload: { pg: path },\n      unique: true,\n    })\n\n    // Assert\n    expect((libInstance as any).sendRequest).toHaveBeenCalledTimes(1)\n    expect((libInstance as any).sendRequest).toHaveBeenCalledWith(\n      expect.any(String),\n      expect.objectContaining({\n        pid: PROJECT_ID,\n        pg: path,\n        unique: true,\n      }),\n    )\n  })\n\n  test('pageview function with metadata should include metadata in request', async () => {\n    // Arrange\n    const path = '/test-page'\n    const metadata = {\n      category: 'blog',\n      author: 'John Doe',\n    }\n\n    // Act\n    pageview({\n      payload: {\n        pg: path,\n        meta: metadata,\n      },\n      unique: false,\n    })\n\n    // Assert\n    expect((libInstance as any).sendRequest).toHaveBeenCalledTimes(1)\n    expect((libInstance as any).sendRequest).toHaveBeenCalledWith(\n      expect.any(String),\n      expect.objectContaining({\n        pid: PROJECT_ID,\n        pg: path,\n        meta: metadata,\n      }),\n    )\n  })\n\n  test('trackViews should start tracking page views', async () => {\n    // Mock the trackPageViews method\n    ;(libInstance as any).trackPageViews = jest.fn().mockReturnValue({\n      stop: jest.fn(),\n    })\n\n    // Act\n    await trackViews()\n\n    // Assert\n    expect((libInstance as any).trackPageViews).toHaveBeenCalledTimes(1)\n  })\n})\n"
  },
  {
    "path": "tests/utils.test.ts",
    "content": "import * as utils from '../src/utils'\n\ndescribe('Utility Functions', () => {\n  beforeEach(() => {\n    Object.defineProperty(window, 'location', {\n      value: {\n        hostname: 'example.com',\n        pathname: '/test-page',\n        hash: '',\n        search: '',\n      },\n      writable: true,\n    })\n\n    Object.defineProperty(document, 'referrer', {\n      value: 'https://google.com',\n      writable: true,\n    })\n\n    Object.defineProperty(navigator, 'language', {\n      value: 'en-US',\n      writable: true,\n    })\n\n    Object.defineProperty(navigator, 'languages', {\n      value: ['en-US', 'en'],\n      writable: true,\n    })\n\n    Object.defineProperty(navigator, 'webdriver', {\n      value: false,\n      writable: true,\n    })\n  })\n\n  test('isInBrowser should return true in JSDOM environment', () => {\n    expect(utils.isInBrowser()).toBe(true)\n  })\n\n  test('isLocalhost should detect localhost', () => {\n    // Arrange\n    Object.defineProperty(window, 'location', {\n      value: {\n        hostname: 'localhost',\n      },\n      writable: true,\n    })\n\n    // Act & Assert\n    expect(utils.isLocalhost()).toBe(true)\n\n    // Test 127.0.0.1\n    Object.defineProperty(window, 'location', {\n      value: {\n        hostname: '127.0.0.1',\n      },\n      writable: true,\n    })\n    expect(utils.isLocalhost()).toBe(true)\n\n    // Test non-localhost\n    Object.defineProperty(window, 'location', {\n      value: {\n        hostname: 'example.com',\n      },\n      writable: true,\n    })\n    expect(utils.isLocalhost()).toBe(false)\n  })\n\n  test('isAutomated should detect webdriver', () => {\n    // Arrange\n    Object.defineProperty(navigator, 'webdriver', {\n      value: true,\n      writable: true,\n    })\n\n    // Act & Assert\n    expect(utils.isAutomated()).toBe(true)\n\n    // Test non-automated\n    Object.defineProperty(navigator, 'webdriver', {\n      value: false,\n      writable: true,\n    })\n    expect(utils.isAutomated()).toBe(false)\n  })\n\n  test('getLocale should return the browser language', () => {\n    expect(utils.getLocale()).toBe('en-US')\n\n    // Test with navigator.languages undefined\n    const originalLanguages = navigator.languages\n    // Instead of deleting, set to undefined\n    Object.defineProperty(navigator, 'languages', {\n      value: undefined,\n      writable: true,\n    })\n    expect(utils.getLocale()).toBe('en-US') // Should use navigator.language as fallback\n\n    // Restore the original value\n    Object.defineProperty(navigator, 'languages', {\n      value: originalLanguages,\n      writable: true,\n    })\n  })\n\n  test('getReferrer should return the document referrer', () => {\n    expect(utils.getReferrer()).toBe('https://google.com')\n\n    // Test with empty referrer\n    Object.defineProperty(document, 'referrer', {\n      value: '',\n      writable: true,\n    })\n    expect(utils.getReferrer()).toBeUndefined()\n  })\n\n  test('getPath should handle different URL formats', () => {\n    // Arrange\n    Object.defineProperty(window, 'location', {\n      value: {\n        pathname: '/test-page',\n        hash: '',\n        search: '',\n      },\n      writable: true,\n    })\n\n    // Act & Assert - basic path\n    expect(utils.getPath({})).toBe('/test-page')\n\n    // Test with hash\n    Object.defineProperty(window, 'location', {\n      value: {\n        pathname: '/test-page',\n        hash: '#section1',\n        search: '',\n      },\n      writable: true,\n    })\n    expect(utils.getPath({ hash: true })).toBe('/test-page#section1')\n\n    // Test with search\n    Object.defineProperty(window, 'location', {\n      value: {\n        pathname: '/test-page',\n        hash: '',\n        search: '?param=value',\n      },\n      writable: true,\n    })\n    expect(utils.getPath({ search: true })).toBe('/test-page?param=value')\n\n    // Test with both hash and search\n    Object.defineProperty(window, 'location', {\n      value: {\n        pathname: '/test-page',\n        hash: '#section1',\n        search: '?param=value',\n      },\n      writable: true,\n    })\n    expect(utils.getPath({ hash: true, search: true })).toBe('/test-page#section1?param=value')\n\n    // Test with search in hash\n    Object.defineProperty(window, 'location', {\n      value: {\n        pathname: '/test-page',\n        hash: '#section1?param=value',\n        search: '',\n      },\n      writable: true,\n    })\n    expect(utils.getPath({ hash: true, search: true })).toBe('/test-page#section1?param=value')\n  })\n\n  test('getUTM* functions should extract UTM parameters', () => {\n    // Arrange\n    Object.defineProperty(window, 'location', {\n      value: {\n        pathname: '/landing',\n        hash: '',\n        search: '?utm_source=google&utm_medium=cpc&utm_campaign=summer&utm_term=analytics&utm_content=ad1',\n      },\n      writable: true,\n    })\n\n    // Act & Assert\n    expect(utils.getUTMSource()).toBe('google')\n    expect(utils.getUTMMedium()).toBe('cpc')\n    expect(utils.getUTMCampaign()).toBe('summer')\n    expect(utils.getUTMTerm()).toBe('analytics')\n    expect(utils.getUTMContent()).toBe('ad1')\n\n    // Test with 'ref' and 'source' parameters as well\n    Object.defineProperty(window, 'location', {\n      value: {\n        pathname: '/landing',\n        hash: '',\n        search: '?ref=twitter',\n      },\n      writable: true,\n    })\n    expect(utils.getUTMSource()).toBe('twitter')\n\n    Object.defineProperty(window, 'location', {\n      value: {\n        pathname: '/landing',\n        hash: '',\n        search: '?source=newsletter',\n      },\n      writable: true,\n    })\n    expect(utils.getUTMSource()).toBe('newsletter')\n  })\n})\n"
  },
  {
    "path": "tsconfig.esnext.json",
    "content": "{\n  \"compilerOptions\": {\n    \"moduleResolution\": \"node\",\n    \"target\": \"ES2020\",\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES2020\", \"DOM\"],\n    \"strict\": true,\n    \"sourceMap\": true,\n    \"declaration\": true,\n    \"outDir\": \"dist/esnext\",\n    \"typeRoots\": [\"node_modules/@types\"]\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"moduleResolution\": \"node\",\n    \"target\": \"ES2018\",\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES2018\", \"DOM\"],\n    \"strict\": true,\n    \"sourceMap\": true,\n    \"declaration\": false,\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  }
]