Full Code of Swetrix/swetrix-js for AI

main ac9244cf3e43 cached
30 files
196.8 KB
48.7k tokens
233 symbols
1 requests
Download .txt
Showing preview only (206K chars total). Download the full file or copy to clipboard to get everything.
Repository: Swetrix/swetrix-js
Branch: main
Commit: ac9244cf3e43
Files: 30
Total size: 196.8 KB

Directory structure:
gitextract_1n1rgte8/

├── .github/
│   ├── funding.yml
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .prettierrc.cjs
├── LICENSE
├── README.md
├── dist/
│   ├── esnext/
│   │   ├── Lib.d.ts
│   │   ├── Lib.js
│   │   ├── index.d.ts
│   │   ├── index.js
│   │   ├── utils.d.ts
│   │   └── utils.js
│   ├── swetrix.cjs.js
│   ├── swetrix.es5.js
│   └── swetrix.js
├── jest.config.js
├── package.json
├── rollup.config.mjs
├── src/
│   ├── Lib.ts
│   ├── index.ts
│   └── utils.ts
├── tests/
│   ├── README.md
│   ├── errors.test.ts
│   ├── events.test.ts
│   ├── experiments.test.ts
│   ├── initialisation.test.ts
│   ├── pageview.test.ts
│   └── utils.test.ts
├── tsconfig.esnext.json
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/funding.yml
================================================
github: swetrix
ko_fi: andriir


================================================
FILE: .github/workflows/test.yml
================================================
name: Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
    types: [opened, labeled, synchronize, ready_for_review]

jobs:
  test:
    name: 🧪 Unit Tests
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [22.x]

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.test

# parcel-bundler cache (https://parceljs.org/)
.cache

# Next.js build output
.next

# Nuxt.js build / generate output
.nuxt
# dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

.DS_Store
pnpm-lock.yaml
yarn.lock


================================================
FILE: .prettierrc.cjs
================================================
/**
 * @see https://prettier.io/docs/configuration
 * @type {import("prettier").Config}
 */
const config = {
  printWidth: 120, // max 120 chars in line, code is easy to read
  useTabs: false, // use spaces instead of tabs
  tabWidth: 2, // "visual width" of of the "tab"
  trailingComma: 'all', // add trailing commas in objects, arrays, etc.
  semi: false, // Only add semicolons at the beginning of lines that may introduce ASI failures
  singleQuote: true, // '' for stings instead of ""
  bracketSpacing: true, // import { some } ... instead of import {some} ...
  arrowParens: 'always', // braces even for single param in arrow functions (a) => { }
  jsxSingleQuote: true, // '' for react props
  bracketSameLine: false, // pretty JSX
  endOfLine: 'lf', // 'lf' for linux, 'crlf' for windows, we need to use 'lf' for git
}

module.exports = config


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 Andrii Romasiun

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://swetrix.com/assets/logo_white.png">
  <img alt="" src="https://swetrix.com/assets/logo_blue.png" height="80">
</picture>
<br /><br />

> [!NOTE]
> 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.

[![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)
[![Package size](https://img.shields.io/bundlephobia/minzip/swetrix)](https://bundlephobia.com/api/size?package=swetrix)
[![Contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/swetrix/swetrix-js/issues)

# Swetrix Tracking Script

This repository contains the analytics script which is used at https://swetrix.com \
You can find the detailed documentation and use cases at our [docs page](https://docs.swetrix.com/).

Feel free to contribute to the source code by opening a pull requests. \
For any questions, you can open an issue ticket, refer to our [FAQs](https://swetrix.com/#faq) page or reach us at contact@swetrix.com

The latest live versions of the script are located at [jsDelivr](https://swetrix.org/swetrix.js) and [NPM](https://www.npmjs.com/package/swetrix).

# Selfhosted API
If you are selfhosting the [Swetrix-API](https://github.com/Swetrix/swetrix-api), be sure to point the `apiUrl` parameter to: `https://yourapiinstance.com/log`

# Donate
You can support the project by donating us at https://ko-fi.com/andriir \
We can only run our services by once again asking for your financial support!


================================================
FILE: dist/esnext/Lib.d.ts
================================================
export interface LibOptions {
    /**
     * When set to `true`, localhost events will be sent to server.
     */
    devMode?: boolean;
    /**
     * When set to `true`, the tracking library won't send any data to server.
     * Useful for development purposes when this value is set based on `.env` var.
     */
    disabled?: boolean;
    /**
     * By setting this flag to `true`, we will not collect ANY kind of data about the user with the DNT setting.
     */
    respectDNT?: boolean;
    /** Set a custom URL of the API server (for selfhosted variants of Swetrix). */
    apiURL?: string;
    /**
     * Optional profile ID for long-term user tracking.
     * If set, it will be used for all pageviews and events unless overridden per-call.
     */
    profileId?: string;
}
export interface TrackEventOptions {
    /** The custom event name. */
    ev: string;
    /** If set to `true`, only 1 event with the same ID will be saved per user session. */
    unique?: boolean;
    /** Event-related metadata object with string values. */
    meta?: {
        [key: string]: string | number | boolean | null | undefined;
    };
    /** Optional profile ID for long-term user tracking. Overrides the global profileId if set. */
    profileId?: string;
}
export interface IPageViewPayload {
    lc?: string;
    tz?: string;
    ref?: string;
    so?: string;
    me?: string;
    ca?: string;
    te?: string;
    co?: string;
    pg?: string | null;
    /** Pageview-related metadata object with string values. */
    meta?: {
        [key: string]: string | number | boolean | null | undefined;
    };
    /** Optional profile ID for long-term user tracking. Overrides the global profileId if set. */
    profileId?: string;
}
export interface IErrorEventPayload {
    name: string;
    message?: string | null;
    lineno?: number | null;
    colno?: number | null;
    filename?: string | null;
    stackTrace?: string | null;
    meta?: {
        [key: string]: string | number | boolean | null | undefined;
    };
}
export interface IInternalErrorEventPayload extends IErrorEventPayload {
    lc?: string;
    tz?: string;
    pg?: string | null;
}
interface IPerfPayload {
    dns: number;
    tls: number;
    conn: number;
    response: number;
    render: number;
    dom_load: number;
    page_load: number;
    ttfb: number;
}
/**
 * Options for evaluating feature flags.
 */
export interface FeatureFlagsOptions {
    /**
     * Optional profile ID for long-term user tracking.
     * If not provided, an anonymous profile ID will be generated server-side based on IP and user agent.
     * Overrides the global profileId if set.
     */
    profileId?: string;
}
/**
 * Options for evaluating experiments.
 */
export interface ExperimentOptions {
    /**
     * Optional profile ID for long-term user tracking.
     * If not provided, an anonymous profile ID will be generated server-side based on IP and user agent.
     * Overrides the global profileId if set.
     */
    profileId?: string;
}
/**
 * The object returned by `trackPageViews()`, used to stop tracking pages.
 */
export interface PageActions {
    /** Stops the tracking of pages. */
    stop: () => void;
}
/**
 * The object returned by `trackErrors()`, used to stop tracking errors.
 */
export interface ErrorActions {
    /** Stops the tracking of errors. */
    stop: () => void;
}
export interface PageData {
    /** Current URL path. */
    path: string;
    /** The object returned by `trackPageViews()`, used to stop tracking pages. */
    actions: PageActions;
}
export interface ErrorOptions {
    /**
     * A number that indicates how many errors should be sent to the server.
     * Accepts values between 0 and 1. For example, if set to 0.5 - only ~50% of errors will be sent to Swetrix.
     * 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.
     *
     * The default value for this option is 1.
     */
    sampleRate?: number;
    /**
     * Callback to edit / prevent sending errors.
     *
     * @param payload - The error payload.
     * @returns The edited payload or `false` to prevent sending the error event. If `true` is returned, the payload will be sent as-is.
     */
    callback?: (payload: IInternalErrorEventPayload) => Partial<IInternalErrorEventPayload> | boolean;
}
export interface PageViewsOptions {
    /**
     * If set to `true`, only unique events will be saved.
     * This param is useful when tracking single-page landing websites.
     */
    unique?: boolean;
    /** Send Heartbeat requests when the website tab is not active in the browser. */
    heartbeatOnBackground?: boolean;
    /**
     * Set to `true` to enable hash-based routing.
     * For example if you have pages like /#/path or want to track pages like /path#hash
     */
    hash?: boolean;
    /**
     * Set to `true` to enable search-based routing.
     * For example if you have pages like /path?search
     */
    search?: boolean;
    /**
     * Callback to edit / prevent sending pageviews.
     *
     * @param payload - The pageview payload.
     * @returns The edited payload or `false` to prevent sending the pageview. If `true` is returned, the payload will be sent as-is.
     */
    callback?: (payload: IPageViewPayload) => Partial<IPageViewPayload> | boolean;
}
export declare const defaultActions: {
    stop(): void;
};
export declare class Lib {
    private projectID;
    private options?;
    private pageData;
    private pageViewsOptions?;
    private errorsOptions?;
    private perfStatsCollected;
    private activePage;
    private errorListenerExists;
    private cachedData;
    constructor(projectID: string, options?: LibOptions | undefined);
    captureError(event: ErrorEvent): void;
    trackErrors(options?: ErrorOptions): ErrorActions;
    submitError(payload: IErrorEventPayload, evokeCallback?: boolean): void;
    track(event: TrackEventOptions): Promise<void>;
    trackPageViews(options?: PageViewsOptions): PageActions;
    getPerformanceStats(): IPerfPayload | {};
    /**
     * Fetches all feature flags and experiments for the project.
     * Results are cached for 5 minutes by default.
     *
     * @param options - Options for evaluating feature flags.
     * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
     * @returns A promise that resolves to a record of flag keys to boolean values.
     */
    getFeatureFlags(options?: FeatureFlagsOptions, forceRefresh?: boolean): Promise<Record<string, boolean>>;
    /**
     * Internal method to fetch both feature flags and experiments from the API.
     */
    private fetchFlagsAndExperiments;
    /**
     * Gets the value of a single feature flag.
     *
     * @param key - The feature flag key.
     * @param options - Options for evaluating the feature flag.
     * @param defaultValue - Default value to return if the flag is not found. Defaults to false.
     * @returns A promise that resolves to the boolean value of the flag.
     */
    getFeatureFlag(key: string, options?: FeatureFlagsOptions, defaultValue?: boolean): Promise<boolean>;
    /**
     * Clears the cached feature flags and experiments, forcing a fresh fetch on the next call.
     */
    clearFeatureFlagsCache(): void;
    /**
     * Fetches all A/B test experiments for the project.
     * Results are cached for 5 minutes by default (shared cache with feature flags).
     *
     * @param options - Options for evaluating experiments.
     * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
     * @returns A promise that resolves to a record of experiment IDs to variant keys.
     *
     * @example
     * ```typescript
     * const experiments = await getExperiments()
     * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }
     * ```
     */
    getExperiments(options?: ExperimentOptions, forceRefresh?: boolean): Promise<Record<string, string>>;
    /**
     * Gets the variant key for a specific A/B test experiment.
     *
     * @param experimentId - The experiment ID.
     * @param options - Options for evaluating the experiment.
     * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.
     * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.
     *
     * @example
     * ```typescript
     * const variant = await getExperiment('checkout-redesign')
     *
     * if (variant === 'new-checkout') {
     *   // Show new checkout flow
     * } else {
     *   // Show control (original) checkout
     * }
     * ```
     */
    getExperiment(experimentId: string, options?: ExperimentOptions, defaultVariant?: string | null): Promise<string | null>;
    /**
     * Clears the cached experiments (alias for clearFeatureFlagsCache since they share the same cache).
     */
    clearExperimentsCache(): void;
    /**
     * Gets the anonymous profile ID for the current visitor.
     * If profileId was set via init options, returns that.
     * Otherwise, requests server to generate one from IP/UA hash.
     *
     * This ID can be used for revenue attribution with payment providers.
     *
     * @returns A promise that resolves to the profile ID string, or null on error.
     *
     * @example
     * ```typescript
     * const profileId = await swetrix.getProfileId()
     *
     * // Pass to Paddle Checkout for revenue attribution
     * Paddle.Checkout.open({
     *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
     *   customData: {
     *     swetrix_profile_id: profileId,
     *     swetrix_session_id: await swetrix.getSessionId()
     *   }
     * })
     * ```
     */
    getProfileId(): Promise<string | null>;
    /**
     * Gets the current session ID for the visitor.
     * Session IDs are generated server-side based on IP and user agent.
     *
     * This ID can be used for revenue attribution with payment providers.
     *
     * @returns A promise that resolves to the session ID string, or null on error.
     *
     * @example
     * ```typescript
     * const sessionId = await swetrix.getSessionId()
     *
     * // Pass to Paddle Checkout for revenue attribution
     * Paddle.Checkout.open({
     *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
     *   customData: {
     *     swetrix_profile_id: await swetrix.getProfileId(),
     *     swetrix_session_id: sessionId
     *   }
     * })
     * ```
     */
    getSessionId(): Promise<string | null>;
    /**
     * Gets the API base URL (without /log suffix).
     */
    private getApiBase;
    private heartbeat;
    private trackPathChange;
    private trackPage;
    submitPageView(payload: Partial<IPageViewPayload>, unique: boolean, perf: IPerfPayload | {}, evokeCallback?: boolean): void;
    private canTrack;
    private sendRequest;
}
export {};


================================================
FILE: dist/esnext/Lib.js
================================================
import { isInBrowser, isLocalhost, isAutomated, getLocale, getTimezone, getReferrer, getUTMCampaign, getUTMMedium, getUTMSource, getUTMTerm, getUTMContent, getPath, } from './utils.js';
export const defaultActions = {
    stop() { },
};
const DEFAULT_API_HOST = 'https://api.swetrix.com/log';
const DEFAULT_API_BASE = 'https://api.swetrix.com';
// Default cache duration: 5 minutes
const DEFAULT_CACHE_DURATION = 5 * 60 * 1000;
export class Lib {
    constructor(projectID, options) {
        this.projectID = projectID;
        this.options = options;
        this.pageData = null;
        this.pageViewsOptions = null;
        this.errorsOptions = null;
        this.perfStatsCollected = false;
        this.activePage = null;
        this.errorListenerExists = false;
        this.cachedData = null;
        this.trackPathChange = this.trackPathChange.bind(this);
        this.heartbeat = this.heartbeat.bind(this);
        this.captureError = this.captureError.bind(this);
    }
    captureError(event) {
        if (typeof this.errorsOptions?.sampleRate === 'number' && this.errorsOptions.sampleRate >= Math.random()) {
            return;
        }
        this.submitError({
            // The file in which error occured.
            filename: event.filename,
            // The line of code error occured on.
            lineno: event.lineno,
            // The column of code error occured on.
            colno: event.colno,
            // 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.
            name: event.error?.name || 'Error',
            // Description of the error. By default, we use message from Error object, is it does not contain the error name
            // (we want to split error name and message so we could group them together later in dashboard).
            // If message in error object does not exist - lets use a message from the Error event itself.
            message: event.error?.message || event.message,
            // Stack trace of the error, if available.
            stackTrace: event.error?.stack,
        }, true);
    }
    trackErrors(options) {
        if (this.errorListenerExists || !this.canTrack()) {
            return defaultActions;
        }
        this.errorsOptions = options;
        window.addEventListener('error', this.captureError);
        this.errorListenerExists = true;
        return {
            stop: () => {
                window.removeEventListener('error', this.captureError);
                this.errorListenerExists = false;
            },
        };
    }
    submitError(payload, evokeCallback) {
        const privateData = {
            pid: this.projectID,
        };
        const errorPayload = {
            pg: this.activePage ||
                getPath({
                    hash: this.pageViewsOptions?.hash,
                    search: this.pageViewsOptions?.search,
                }),
            lc: getLocale(),
            tz: getTimezone(),
            ...payload,
        };
        if (evokeCallback && this.errorsOptions?.callback) {
            const callbackResult = this.errorsOptions.callback(errorPayload);
            if (callbackResult === false) {
                return;
            }
            if (callbackResult && typeof callbackResult === 'object') {
                Object.assign(errorPayload, callbackResult);
            }
        }
        Object.assign(errorPayload, privateData);
        this.sendRequest('error', errorPayload);
    }
    async track(event) {
        if (!this.canTrack()) {
            return;
        }
        const data = {
            ...event,
            pid: this.projectID,
            pg: this.activePage ||
                getPath({
                    hash: this.pageViewsOptions?.hash,
                    search: this.pageViewsOptions?.search,
                }),
            lc: getLocale(),
            tz: getTimezone(),
            ref: getReferrer(),
            so: getUTMSource(),
            me: getUTMMedium(),
            ca: getUTMCampaign(),
            te: getUTMTerm(),
            co: getUTMContent(),
            profileId: event.profileId ?? this.options?.profileId,
        };
        await this.sendRequest('custom', data);
    }
    trackPageViews(options) {
        if (!this.canTrack()) {
            return defaultActions;
        }
        if (this.pageData) {
            return this.pageData.actions;
        }
        this.pageViewsOptions = options;
        let interval;
        if (!options?.unique) {
            interval = setInterval(this.trackPathChange, 2000);
        }
        setTimeout(this.heartbeat, 3000);
        const hbInterval = setInterval(this.heartbeat, 28000);
        const path = getPath({
            hash: options?.hash,
            search: options?.search,
        });
        this.pageData = {
            path,
            actions: {
                stop: () => {
                    clearInterval(interval);
                    clearInterval(hbInterval);
                },
            },
        };
        this.trackPage(path, options?.unique);
        return this.pageData.actions;
    }
    getPerformanceStats() {
        if (!this.canTrack() || this.perfStatsCollected || !window.performance?.getEntriesByType) {
            return {};
        }
        const perf = window.performance.getEntriesByType('navigation')[0];
        if (!perf) {
            return {};
        }
        this.perfStatsCollected = true;
        return {
            // Network
            dns: perf.domainLookupEnd - perf.domainLookupStart, // DNS Resolution
            tls: perf.secureConnectionStart ? perf.requestStart - perf.secureConnectionStart : 0, // TLS Setup; checking if secureConnectionStart is not 0 (it's 0 for non-https websites)
            conn: perf.secureConnectionStart
                ? perf.secureConnectionStart - perf.connectStart
                : perf.connectEnd - perf.connectStart, // Connection time
            response: perf.responseEnd - perf.responseStart, // Response Time (Download)
            // Frontend
            render: perf.domComplete - perf.domContentLoadedEventEnd, // Browser rendering the HTML time
            dom_load: perf.domContentLoadedEventEnd - perf.responseEnd, // DOM loading timing
            page_load: perf.loadEventStart, // Page load time
            // Backend
            ttfb: perf.responseStart - perf.requestStart,
        };
    }
    /**
     * Fetches all feature flags and experiments for the project.
     * Results are cached for 5 minutes by default.
     *
     * @param options - Options for evaluating feature flags.
     * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
     * @returns A promise that resolves to a record of flag keys to boolean values.
     */
    async getFeatureFlags(options, forceRefresh) {
        if (!isInBrowser()) {
            return {};
        }
        const requestedProfileId = options?.profileId ?? this.options?.profileId;
        // Check cache first - must match profileId and not be expired
        if (!forceRefresh && this.cachedData) {
            const now = Date.now();
            const isSameProfile = this.cachedData.profileId === requestedProfileId;
            if (isSameProfile && now - this.cachedData.timestamp < DEFAULT_CACHE_DURATION) {
                return this.cachedData.flags;
            }
        }
        try {
            await this.fetchFlagsAndExperiments(options);
            return this.cachedData?.flags || {};
        }
        catch (error) {
            console.warn('[Swetrix] Error fetching feature flags:', error);
            return this.cachedData?.flags || {};
        }
    }
    /**
     * Internal method to fetch both feature flags and experiments from the API.
     */
    async fetchFlagsAndExperiments(options) {
        const apiBase = this.getApiBase();
        const body = {
            pid: this.projectID,
        };
        // Use profileId from options, or fall back to global profileId
        const profileId = options?.profileId ?? this.options?.profileId;
        if (profileId) {
            body.profileId = profileId;
        }
        const response = await fetch(`${apiBase}/feature-flag/evaluate`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(body),
        });
        if (!response.ok) {
            console.warn('[Swetrix] Failed to fetch feature flags and experiments:', response.status);
            return;
        }
        const data = (await response.json());
        // Use profileId from options, or fall back to global profileId
        const cachedProfileId = options?.profileId ?? this.options?.profileId;
        // Update cache with both flags and experiments
        this.cachedData = {
            flags: data.flags || {},
            experiments: data.experiments || {},
            timestamp: Date.now(),
            profileId: cachedProfileId,
        };
    }
    /**
     * Gets the value of a single feature flag.
     *
     * @param key - The feature flag key.
     * @param options - Options for evaluating the feature flag.
     * @param defaultValue - Default value to return if the flag is not found. Defaults to false.
     * @returns A promise that resolves to the boolean value of the flag.
     */
    async getFeatureFlag(key, options, defaultValue = false) {
        const flags = await this.getFeatureFlags(options);
        return flags[key] ?? defaultValue;
    }
    /**
     * Clears the cached feature flags and experiments, forcing a fresh fetch on the next call.
     */
    clearFeatureFlagsCache() {
        this.cachedData = null;
    }
    /**
     * Fetches all A/B test experiments for the project.
     * Results are cached for 5 minutes by default (shared cache with feature flags).
     *
     * @param options - Options for evaluating experiments.
     * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
     * @returns A promise that resolves to a record of experiment IDs to variant keys.
     *
     * @example
     * ```typescript
     * const experiments = await getExperiments()
     * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }
     * ```
     */
    async getExperiments(options, forceRefresh) {
        if (!isInBrowser()) {
            return {};
        }
        const requestedProfileId = options?.profileId ?? this.options?.profileId;
        // Check cache first - must match profileId and not be expired
        if (!forceRefresh && this.cachedData) {
            const now = Date.now();
            const isSameProfile = this.cachedData.profileId === requestedProfileId;
            if (isSameProfile && now - this.cachedData.timestamp < DEFAULT_CACHE_DURATION) {
                return this.cachedData.experiments;
            }
        }
        try {
            await this.fetchFlagsAndExperiments(options);
            return this.cachedData?.experiments || {};
        }
        catch (error) {
            console.warn('[Swetrix] Error fetching experiments:', error);
            return this.cachedData?.experiments || {};
        }
    }
    /**
     * Gets the variant key for a specific A/B test experiment.
     *
     * @param experimentId - The experiment ID.
     * @param options - Options for evaluating the experiment.
     * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.
     * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.
     *
     * @example
     * ```typescript
     * const variant = await getExperiment('checkout-redesign')
     *
     * if (variant === 'new-checkout') {
     *   // Show new checkout flow
     * } else {
     *   // Show control (original) checkout
     * }
     * ```
     */
    async getExperiment(experimentId, options, defaultVariant = null) {
        const experiments = await this.getExperiments(options);
        return experiments[experimentId] ?? defaultVariant;
    }
    /**
     * Clears the cached experiments (alias for clearFeatureFlagsCache since they share the same cache).
     */
    clearExperimentsCache() {
        this.cachedData = null;
    }
    /**
     * Gets the anonymous profile ID for the current visitor.
     * If profileId was set via init options, returns that.
     * Otherwise, requests server to generate one from IP/UA hash.
     *
     * This ID can be used for revenue attribution with payment providers.
     *
     * @returns A promise that resolves to the profile ID string, or null on error.
     *
     * @example
     * ```typescript
     * const profileId = await swetrix.getProfileId()
     *
     * // Pass to Paddle Checkout for revenue attribution
     * Paddle.Checkout.open({
     *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
     *   customData: {
     *     swetrix_profile_id: profileId,
     *     swetrix_session_id: await swetrix.getSessionId()
     *   }
     * })
     * ```
     */
    async getProfileId() {
        // If profileId is already set in options, return it
        if (this.options?.profileId) {
            return this.options.profileId;
        }
        if (!isInBrowser()) {
            return null;
        }
        try {
            const apiBase = this.getApiBase();
            const response = await fetch(`${apiBase}/log/profile-id`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ pid: this.projectID }),
            });
            if (!response.ok) {
                return null;
            }
            const data = (await response.json());
            return data.profileId;
        }
        catch {
            return null;
        }
    }
    /**
     * Gets the current session ID for the visitor.
     * Session IDs are generated server-side based on IP and user agent.
     *
     * This ID can be used for revenue attribution with payment providers.
     *
     * @returns A promise that resolves to the session ID string, or null on error.
     *
     * @example
     * ```typescript
     * const sessionId = await swetrix.getSessionId()
     *
     * // Pass to Paddle Checkout for revenue attribution
     * Paddle.Checkout.open({
     *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
     *   customData: {
     *     swetrix_profile_id: await swetrix.getProfileId(),
     *     swetrix_session_id: sessionId
     *   }
     * })
     * ```
     */
    async getSessionId() {
        if (!isInBrowser()) {
            return null;
        }
        try {
            const apiBase = this.getApiBase();
            const response = await fetch(`${apiBase}/log/session-id`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ pid: this.projectID }),
            });
            if (!response.ok) {
                return null;
            }
            const data = (await response.json());
            return data.sessionId;
        }
        catch {
            return null;
        }
    }
    /**
     * Gets the API base URL (without /log suffix).
     */
    getApiBase() {
        if (this.options?.apiURL) {
            // Remove trailing /log if present
            return this.options.apiURL.replace(/\/log\/?$/, '');
        }
        return DEFAULT_API_BASE;
    }
    heartbeat() {
        if (!this.pageViewsOptions?.heartbeatOnBackground && document.visibilityState === 'hidden') {
            return;
        }
        const data = {
            pid: this.projectID,
        };
        if (this.options?.profileId) {
            data.profileId = this.options.profileId;
        }
        this.sendRequest('hb', data);
    }
    // Tracking path changes. If path changes -> calling this.trackPage method
    trackPathChange() {
        if (!this.pageData)
            return;
        const newPath = getPath({
            hash: this.pageViewsOptions?.hash,
            search: this.pageViewsOptions?.search,
        });
        const { path } = this.pageData;
        if (path !== newPath) {
            this.trackPage(newPath, false);
        }
    }
    trackPage(pg, unique = false) {
        if (!this.pageData)
            return;
        this.pageData.path = pg;
        const perf = this.getPerformanceStats();
        this.activePage = pg;
        this.submitPageView({ pg }, unique, perf, true);
    }
    submitPageView(payload, unique, perf, evokeCallback) {
        const privateData = {
            pid: this.projectID,
            perf,
            unique,
        };
        const pvPayload = {
            lc: getLocale(),
            tz: getTimezone(),
            ref: getReferrer(),
            so: getUTMSource(),
            me: getUTMMedium(),
            ca: getUTMCampaign(),
            te: getUTMTerm(),
            co: getUTMContent(),
            profileId: this.options?.profileId,
            ...payload,
        };
        if (evokeCallback && this.pageViewsOptions?.callback) {
            const callbackResult = this.pageViewsOptions.callback(pvPayload);
            if (callbackResult === false) {
                return;
            }
            if (callbackResult && typeof callbackResult === 'object') {
                Object.assign(pvPayload, callbackResult);
            }
        }
        Object.assign(pvPayload, privateData);
        this.sendRequest('', pvPayload);
    }
    canTrack() {
        if (this.options?.disabled ||
            !isInBrowser() ||
            (this.options?.respectDNT && window.navigator?.doNotTrack === '1') ||
            (!this.options?.devMode && isLocalhost()) ||
            isAutomated()) {
            return false;
        }
        return true;
    }
    async sendRequest(path, body) {
        const host = this.options?.apiURL || DEFAULT_API_HOST;
        await fetch(`${host}/${path}`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(body),
        });
    }
}
//# sourceMappingURL=Lib.js.map

================================================
FILE: dist/esnext/index.d.ts
================================================
import { Lib, LibOptions, TrackEventOptions, PageViewsOptions, ErrorOptions, PageActions, ErrorActions, IErrorEventPayload, IPageViewPayload, FeatureFlagsOptions, ExperimentOptions } from './Lib.js';
export declare let LIB_INSTANCE: Lib | null;
/**
 * Initialise the tracking library instance (other methods won't work if the library is not initialised).
 *
 * @param {string} pid The Project ID to link the instance of Swetrix.js to.
 * @param {LibOptions} options Options related to the tracking.
 * @returns {Lib} Instance of the Swetrix.js.
 */
export declare function init(pid: string, options?: LibOptions): Lib;
/**
 * With this function you are able to track any custom events you want.
 * You should never send any identifiable data (like User ID, email, session cookie, etc.) as an event name.
 * The total number of track calls and their conversion rate will be saved.
 *
 * @param {TrackEventOptions} event The options related to the custom event.
 */
export declare function track(event: TrackEventOptions): Promise<void>;
/**
 * With this function you are able to automatically track pageviews across your application.
 *
 * @param {PageViewsOptions} options Pageviews tracking options.
 * @returns {PageActions} The actions related to the tracking. Used to stop tracking pages.
 */
export declare function trackViews(options?: PageViewsOptions): Promise<PageActions>;
/**
 * This function is used to set up automatic error events tracking.
 * It set's up an error listener, and whenever an error happens, it gets tracked.
 *
 * @returns {ErrorActions} The actions related to the tracking. Used to stop tracking errors.
 */
export declare function trackErrors(options?: ErrorOptions): ErrorActions;
/**
 * This function is used to manually track an error event.
 * It's useful if you want to track specific errors in your application.
 *
 * @param payload Swetrix error object to send.
 * @returns void
 */
export declare function trackError(payload: IErrorEventPayload): void;
/**
 * This function is used to manually track a page view event.
 * It's useful if your application uses esoteric routing which is not supported by Swetrix by default.
 *
 * @deprecated This function is deprecated and will be removed soon, please use the `pageview` instead.
 * @param pg Path of the page to track (this will be sent to the Swetrix API and displayed in the dashboard).
 * @param _prev Path of the previous page (deprecated and ignored).
 * @param unique If set to `true`, only 1 event with the same ID will be saved per user session.
 * @returns void
 */
export declare function trackPageview(pg: string, _prev?: string, unique?: boolean): void;
export interface IPageviewOptions {
    payload: Partial<IPageViewPayload>;
    unique?: boolean;
}
export declare function pageview(options: IPageviewOptions): void;
/**
 * Fetches all feature flags for the project.
 * Results are cached for 5 minutes by default.
 *
 * @param options - Options for evaluating feature flags (visitorId, attributes).
 * @param forceRefresh - If true, bypasses the cache and fetches fresh flags.
 * @returns A promise that resolves to a record of flag keys to boolean values.
 *
 * @example
 * ```typescript
 * const flags = await getFeatureFlags({
 *   visitorId: 'user-123',
 *   attributes: { cc: 'US', dv: 'desktop' }
 * })
 *
 * if (flags['new-checkout']) {
 *   // Show new checkout flow
 * }
 * ```
 */
export declare function getFeatureFlags(options?: FeatureFlagsOptions, forceRefresh?: boolean): Promise<Record<string, boolean>>;
/**
 * Gets the value of a single feature flag.
 *
 * @param key - The feature flag key.
 * @param options - Options for evaluating the feature flag (visitorId, attributes).
 * @param defaultValue - Default value to return if the flag is not found. Defaults to false.
 * @returns A promise that resolves to the boolean value of the flag.
 *
 * @example
 * ```typescript
 * const isEnabled = await getFeatureFlag('dark-mode', { visitorId: 'user-123' })
 *
 * if (isEnabled) {
 *   // Enable dark mode
 * }
 * ```
 */
export declare function getFeatureFlag(key: string, options?: FeatureFlagsOptions, defaultValue?: boolean): Promise<boolean>;
/**
 * Clears the cached feature flags, forcing a fresh fetch on the next call.
 * Useful when you know the user's context has changed significantly.
 */
export declare function clearFeatureFlagsCache(): void;
/**
 * Fetches all A/B test experiments for the project.
 * Results are cached for 5 minutes by default (shared cache with feature flags).
 *
 * @param options - Options for evaluating experiments.
 * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
 * @returns A promise that resolves to a record of experiment IDs to variant keys.
 *
 * @example
 * ```typescript
 * const experiments = await getExperiments()
 * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }
 *
 * // Use the assigned variant
 * const checkoutVariant = experiments['checkout-experiment-id']
 * if (checkoutVariant === 'new-checkout') {
 *   showNewCheckout()
 * } else {
 *   showOriginalCheckout()
 * }
 * ```
 */
export declare function getExperiments(options?: ExperimentOptions, forceRefresh?: boolean): Promise<Record<string, string>>;
/**
 * Gets the variant key for a specific A/B test experiment.
 *
 * @param experimentId - The experiment ID.
 * @param options - Options for evaluating the experiment.
 * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.
 * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.
 *
 * @example
 * ```typescript
 * const variant = await getExperiment('checkout-redesign-experiment-id')
 *
 * if (variant === 'new-checkout') {
 *   // Show new checkout flow
 *   showNewCheckout()
 * } else if (variant === 'control') {
 *   // Show original checkout (control group)
 *   showOriginalCheckout()
 * } else {
 *   // Experiment not running or user not included
 *   showOriginalCheckout()
 * }
 * ```
 */
export declare function getExperiment(experimentId: string, options?: ExperimentOptions, defaultVariant?: string | null): Promise<string | null>;
/**
 * Clears the cached experiments, forcing a fresh fetch on the next call.
 * This is an alias for clearFeatureFlagsCache since experiments and flags share the same cache.
 */
export declare function clearExperimentsCache(): void;
/**
 * Gets the anonymous profile ID for the current visitor.
 * If profileId was set via init options, returns that.
 * Otherwise, requests server to generate one from IP/UA hash.
 *
 * This ID can be used for revenue attribution with payment providers like Paddle.
 *
 * @returns A promise that resolves to the profile ID string, or null on error.
 *
 * @example
 * ```typescript
 * const profileId = await getProfileId()
 *
 * // Pass to Paddle Checkout for revenue attribution
 * Paddle.Checkout.open({
 *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
 *   customData: {
 *     swetrix_profile_id: profileId,
 *     swetrix_session_id: await getSessionId()
 *   }
 * })
 * ```
 */
export declare function getProfileId(): Promise<string | null>;
/**
 * Gets the current session ID for the visitor.
 * Session IDs are generated server-side based on IP and user agent.
 *
 * This ID can be used for revenue attribution with payment providers like Paddle.
 *
 * @returns A promise that resolves to the session ID string, or null on error.
 *
 * @example
 * ```typescript
 * const sessionId = await getSessionId()
 *
 * // Pass to Paddle Checkout for revenue attribution
 * Paddle.Checkout.open({
 *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
 *   customData: {
 *     swetrix_profile_id: await getProfileId(),
 *     swetrix_session_id: sessionId
 *   }
 * })
 * ```
 */
export declare function getSessionId(): Promise<string | null>;
export { LibOptions, TrackEventOptions, PageViewsOptions, ErrorOptions, PageActions, ErrorActions, IErrorEventPayload, IPageViewPayload, FeatureFlagsOptions, ExperimentOptions, };


================================================
FILE: dist/esnext/index.js
================================================
import { Lib, defaultActions, } from './Lib.js';
export let LIB_INSTANCE = null;
/**
 * Initialise the tracking library instance (other methods won't work if the library is not initialised).
 *
 * @param {string} pid The Project ID to link the instance of Swetrix.js to.
 * @param {LibOptions} options Options related to the tracking.
 * @returns {Lib} Instance of the Swetrix.js.
 */
export function init(pid, options) {
    if (!LIB_INSTANCE) {
        LIB_INSTANCE = new Lib(pid, options);
    }
    return LIB_INSTANCE;
}
/**
 * With this function you are able to track any custom events you want.
 * You should never send any identifiable data (like User ID, email, session cookie, etc.) as an event name.
 * The total number of track calls and their conversion rate will be saved.
 *
 * @param {TrackEventOptions} event The options related to the custom event.
 */
export async function track(event) {
    if (!LIB_INSTANCE)
        return;
    await LIB_INSTANCE.track(event);
}
/**
 * With this function you are able to automatically track pageviews across your application.
 *
 * @param {PageViewsOptions} options Pageviews tracking options.
 * @returns {PageActions} The actions related to the tracking. Used to stop tracking pages.
 */
export function trackViews(options) {
    return new Promise((resolve) => {
        if (!LIB_INSTANCE) {
            resolve(defaultActions);
            return;
        }
        // We need to verify that document.readyState is complete for the performance stats to be collected correctly.
        if (typeof document === 'undefined' || document.readyState === 'complete') {
            resolve(LIB_INSTANCE.trackPageViews(options));
        }
        else {
            window.addEventListener('load', () => {
                // @ts-ignore
                resolve(LIB_INSTANCE.trackPageViews(options));
            });
        }
    });
}
/**
 * This function is used to set up automatic error events tracking.
 * It set's up an error listener, and whenever an error happens, it gets tracked.
 *
 * @returns {ErrorActions} The actions related to the tracking. Used to stop tracking errors.
 */
export function trackErrors(options) {
    if (!LIB_INSTANCE) {
        return defaultActions;
    }
    return LIB_INSTANCE.trackErrors(options);
}
/**
 * This function is used to manually track an error event.
 * It's useful if you want to track specific errors in your application.
 *
 * @param payload Swetrix error object to send.
 * @returns void
 */
export function trackError(payload) {
    if (!LIB_INSTANCE)
        return;
    LIB_INSTANCE.submitError(payload, false);
}
/**
 * This function is used to manually track a page view event.
 * It's useful if your application uses esoteric routing which is not supported by Swetrix by default.
 *
 * @deprecated This function is deprecated and will be removed soon, please use the `pageview` instead.
 * @param pg Path of the page to track (this will be sent to the Swetrix API and displayed in the dashboard).
 * @param _prev Path of the previous page (deprecated and ignored).
 * @param unique If set to `true`, only 1 event with the same ID will be saved per user session.
 * @returns void
 */
export function trackPageview(pg, _prev, unique) {
    if (!LIB_INSTANCE)
        return;
    LIB_INSTANCE.submitPageView({ pg }, Boolean(unique), {});
}
export function pageview(options) {
    if (!LIB_INSTANCE)
        return;
    LIB_INSTANCE.submitPageView(options.payload, Boolean(options.unique), {});
}
/**
 * Fetches all feature flags for the project.
 * Results are cached for 5 minutes by default.
 *
 * @param options - Options for evaluating feature flags (visitorId, attributes).
 * @param forceRefresh - If true, bypasses the cache and fetches fresh flags.
 * @returns A promise that resolves to a record of flag keys to boolean values.
 *
 * @example
 * ```typescript
 * const flags = await getFeatureFlags({
 *   visitorId: 'user-123',
 *   attributes: { cc: 'US', dv: 'desktop' }
 * })
 *
 * if (flags['new-checkout']) {
 *   // Show new checkout flow
 * }
 * ```
 */
export async function getFeatureFlags(options, forceRefresh) {
    if (!LIB_INSTANCE)
        return {};
    return LIB_INSTANCE.getFeatureFlags(options, forceRefresh);
}
/**
 * Gets the value of a single feature flag.
 *
 * @param key - The feature flag key.
 * @param options - Options for evaluating the feature flag (visitorId, attributes).
 * @param defaultValue - Default value to return if the flag is not found. Defaults to false.
 * @returns A promise that resolves to the boolean value of the flag.
 *
 * @example
 * ```typescript
 * const isEnabled = await getFeatureFlag('dark-mode', { visitorId: 'user-123' })
 *
 * if (isEnabled) {
 *   // Enable dark mode
 * }
 * ```
 */
export async function getFeatureFlag(key, options, defaultValue = false) {
    if (!LIB_INSTANCE)
        return defaultValue;
    return LIB_INSTANCE.getFeatureFlag(key, options, defaultValue);
}
/**
 * Clears the cached feature flags, forcing a fresh fetch on the next call.
 * Useful when you know the user's context has changed significantly.
 */
export function clearFeatureFlagsCache() {
    if (!LIB_INSTANCE)
        return;
    LIB_INSTANCE.clearFeatureFlagsCache();
}
/**
 * Fetches all A/B test experiments for the project.
 * Results are cached for 5 minutes by default (shared cache with feature flags).
 *
 * @param options - Options for evaluating experiments.
 * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
 * @returns A promise that resolves to a record of experiment IDs to variant keys.
 *
 * @example
 * ```typescript
 * const experiments = await getExperiments()
 * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }
 *
 * // Use the assigned variant
 * const checkoutVariant = experiments['checkout-experiment-id']
 * if (checkoutVariant === 'new-checkout') {
 *   showNewCheckout()
 * } else {
 *   showOriginalCheckout()
 * }
 * ```
 */
export async function getExperiments(options, forceRefresh) {
    if (!LIB_INSTANCE)
        return {};
    return LIB_INSTANCE.getExperiments(options, forceRefresh);
}
/**
 * Gets the variant key for a specific A/B test experiment.
 *
 * @param experimentId - The experiment ID.
 * @param options - Options for evaluating the experiment.
 * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.
 * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.
 *
 * @example
 * ```typescript
 * const variant = await getExperiment('checkout-redesign-experiment-id')
 *
 * if (variant === 'new-checkout') {
 *   // Show new checkout flow
 *   showNewCheckout()
 * } else if (variant === 'control') {
 *   // Show original checkout (control group)
 *   showOriginalCheckout()
 * } else {
 *   // Experiment not running or user not included
 *   showOriginalCheckout()
 * }
 * ```
 */
export async function getExperiment(experimentId, options, defaultVariant = null) {
    if (!LIB_INSTANCE)
        return defaultVariant;
    return LIB_INSTANCE.getExperiment(experimentId, options, defaultVariant);
}
/**
 * Clears the cached experiments, forcing a fresh fetch on the next call.
 * This is an alias for clearFeatureFlagsCache since experiments and flags share the same cache.
 */
export function clearExperimentsCache() {
    if (!LIB_INSTANCE)
        return;
    LIB_INSTANCE.clearExperimentsCache();
}
/**
 * Gets the anonymous profile ID for the current visitor.
 * If profileId was set via init options, returns that.
 * Otherwise, requests server to generate one from IP/UA hash.
 *
 * This ID can be used for revenue attribution with payment providers like Paddle.
 *
 * @returns A promise that resolves to the profile ID string, or null on error.
 *
 * @example
 * ```typescript
 * const profileId = await getProfileId()
 *
 * // Pass to Paddle Checkout for revenue attribution
 * Paddle.Checkout.open({
 *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
 *   customData: {
 *     swetrix_profile_id: profileId,
 *     swetrix_session_id: await getSessionId()
 *   }
 * })
 * ```
 */
export async function getProfileId() {
    if (!LIB_INSTANCE)
        return null;
    return LIB_INSTANCE.getProfileId();
}
/**
 * Gets the current session ID for the visitor.
 * Session IDs are generated server-side based on IP and user agent.
 *
 * This ID can be used for revenue attribution with payment providers like Paddle.
 *
 * @returns A promise that resolves to the session ID string, or null on error.
 *
 * @example
 * ```typescript
 * const sessionId = await getSessionId()
 *
 * // Pass to Paddle Checkout for revenue attribution
 * Paddle.Checkout.open({
 *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
 *   customData: {
 *     swetrix_profile_id: await getProfileId(),
 *     swetrix_session_id: sessionId
 *   }
 * })
 * ```
 */
export async function getSessionId() {
    if (!LIB_INSTANCE)
        return null;
    return LIB_INSTANCE.getSessionId();
}
//# sourceMappingURL=index.js.map

================================================
FILE: dist/esnext/utils.d.ts
================================================
interface IGetPath {
    hash?: boolean;
    search?: boolean;
}
export declare const isInBrowser: () => boolean;
export declare const isLocalhost: () => boolean;
export declare const isAutomated: () => boolean;
export declare const getLocale: () => string;
export declare const getTimezone: () => string | undefined;
export declare const getReferrer: () => string | undefined;
export declare const getUTMSource: () => string | undefined;
export declare const getUTMMedium: () => string | undefined;
export declare const getUTMCampaign: () => string | undefined;
export declare const getUTMTerm: () => string | undefined;
export declare const getUTMContent: () => string | undefined;
/**
 * Function used to track the current page (path) of the application.
 * Will work in cases where the path looks like:
 * - /path
 * - /#/path
 * - /path?search
 * - /path?search#hash
 * - /path#hash?search
 *
 * @param options - Options for the function.
 * @param options.hash - Whether to trigger on hash change.
 * @param options.search - Whether to trigger on search change.
 * @returns The path of the current page.
 */
export declare const getPath: (options: IGetPath) => string;
export {};


================================================
FILE: dist/esnext/utils.js
================================================
const findInSearch = (exp) => {
    const res = location.search.match(exp);
    return (res && res[2]) || undefined;
};
const utmSourceRegex = /[?&](ref|source|utm_source|gad_source)=([^?&]+)/;
const utmCampaignRegex = /[?&](utm_campaign|gad_campaignid)=([^?&]+)/;
const utmMediumRegex = /[?&](utm_medium)=([^?&]+)/;
const utmTermRegex = /[?&](utm_term)=([^?&]+)/;
const utmContentRegex = /[?&](utm_content)=([^?&]+)/;
const gclidRegex = /[?&](gclid)=([^?&]+)/;
const getGclid = () => {
    return findInSearch(gclidRegex) ? '<gclid>' : undefined;
};
export const isInBrowser = () => {
    return typeof window !== 'undefined';
};
export const isLocalhost = () => {
    return location?.hostname === 'localhost' || location?.hostname === '127.0.0.1' || location?.hostname === '';
};
export const isAutomated = () => {
    return navigator?.webdriver;
};
export const getLocale = () => {
    return typeof navigator.languages !== 'undefined' ? navigator.languages[0] : navigator.language;
};
export const getTimezone = () => {
    try {
        return Intl.DateTimeFormat().resolvedOptions().timeZone;
    }
    catch (e) {
        return;
    }
};
export const getReferrer = () => {
    return document.referrer || undefined;
};
export const getUTMSource = () => findInSearch(utmSourceRegex);
export const getUTMMedium = () => findInSearch(utmMediumRegex) || getGclid();
export const getUTMCampaign = () => findInSearch(utmCampaignRegex);
export const getUTMTerm = () => findInSearch(utmTermRegex);
export const getUTMContent = () => findInSearch(utmContentRegex);
/**
 * Function used to track the current page (path) of the application.
 * Will work in cases where the path looks like:
 * - /path
 * - /#/path
 * - /path?search
 * - /path?search#hash
 * - /path#hash?search
 *
 * @param options - Options for the function.
 * @param options.hash - Whether to trigger on hash change.
 * @param options.search - Whether to trigger on search change.
 * @returns The path of the current page.
 */
export const getPath = (options) => {
    let result = location.pathname || '';
    if (options.hash) {
        const hashIndex = location.hash.indexOf('?');
        const hashString = hashIndex > -1 ? location.hash.substring(0, hashIndex) : location.hash;
        result += hashString;
    }
    if (options.search) {
        const hashIndex = location.hash.indexOf('?');
        const searchString = location.search || (hashIndex > -1 ? location.hash.substring(hashIndex) : '');
        result += searchString;
    }
    return result;
};
//# sourceMappingURL=utils.js.map

================================================
FILE: dist/swetrix.cjs.js
================================================
'use strict';

const findInSearch = (exp) => {
    const res = location.search.match(exp);
    return (res && res[2]) || undefined;
};
const utmSourceRegex = /[?&](ref|source|utm_source|gad_source)=([^?&]+)/;
const utmCampaignRegex = /[?&](utm_campaign|gad_campaignid)=([^?&]+)/;
const utmMediumRegex = /[?&](utm_medium)=([^?&]+)/;
const utmTermRegex = /[?&](utm_term)=([^?&]+)/;
const utmContentRegex = /[?&](utm_content)=([^?&]+)/;
const gclidRegex = /[?&](gclid)=([^?&]+)/;
const getGclid = () => {
    return findInSearch(gclidRegex) ? '<gclid>' : undefined;
};
const isInBrowser = () => {
    return typeof window !== 'undefined';
};
const isLocalhost = () => {
    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) === '';
};
const isAutomated = () => {
    return navigator === null || navigator === void 0 ? void 0 : navigator.webdriver;
};
const getLocale = () => {
    return typeof navigator.languages !== 'undefined' ? navigator.languages[0] : navigator.language;
};
const getTimezone = () => {
    try {
        return Intl.DateTimeFormat().resolvedOptions().timeZone;
    }
    catch (e) {
        return;
    }
};
const getReferrer = () => {
    return document.referrer || undefined;
};
const getUTMSource = () => findInSearch(utmSourceRegex);
const getUTMMedium = () => findInSearch(utmMediumRegex) || getGclid();
const getUTMCampaign = () => findInSearch(utmCampaignRegex);
const getUTMTerm = () => findInSearch(utmTermRegex);
const getUTMContent = () => findInSearch(utmContentRegex);
/**
 * Function used to track the current page (path) of the application.
 * Will work in cases where the path looks like:
 * - /path
 * - /#/path
 * - /path?search
 * - /path?search#hash
 * - /path#hash?search
 *
 * @param options - Options for the function.
 * @param options.hash - Whether to trigger on hash change.
 * @param options.search - Whether to trigger on search change.
 * @returns The path of the current page.
 */
const getPath = (options) => {
    let result = location.pathname || '';
    if (options.hash) {
        const hashIndex = location.hash.indexOf('?');
        const hashString = hashIndex > -1 ? location.hash.substring(0, hashIndex) : location.hash;
        result += hashString;
    }
    if (options.search) {
        const hashIndex = location.hash.indexOf('?');
        const searchString = location.search || (hashIndex > -1 ? location.hash.substring(hashIndex) : '');
        result += searchString;
    }
    return result;
};

const defaultActions = {
    stop() { },
};
const DEFAULT_API_HOST = 'https://api.swetrix.com/log';
const DEFAULT_API_BASE = 'https://api.swetrix.com';
// Default cache duration: 5 minutes
const DEFAULT_CACHE_DURATION = 5 * 60 * 1000;
class Lib {
    constructor(projectID, options) {
        this.projectID = projectID;
        this.options = options;
        this.pageData = null;
        this.pageViewsOptions = null;
        this.errorsOptions = null;
        this.perfStatsCollected = false;
        this.activePage = null;
        this.errorListenerExists = false;
        this.cachedData = null;
        this.trackPathChange = this.trackPathChange.bind(this);
        this.heartbeat = this.heartbeat.bind(this);
        this.captureError = this.captureError.bind(this);
    }
    captureError(event) {
        var _a, _b, _c, _d;
        if (typeof ((_a = this.errorsOptions) === null || _a === void 0 ? void 0 : _a.sampleRate) === 'number' && this.errorsOptions.sampleRate >= Math.random()) {
            return;
        }
        this.submitError({
            // The file in which error occured.
            filename: event.filename,
            // The line of code error occured on.
            lineno: event.lineno,
            // The column of code error occured on.
            colno: event.colno,
            // 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.
            name: ((_b = event.error) === null || _b === void 0 ? void 0 : _b.name) || 'Error',
            // Description of the error. By default, we use message from Error object, is it does not contain the error name
            // (we want to split error name and message so we could group them together later in dashboard).
            // If message in error object does not exist - lets use a message from the Error event itself.
            message: ((_c = event.error) === null || _c === void 0 ? void 0 : _c.message) || event.message,
            // Stack trace of the error, if available.
            stackTrace: (_d = event.error) === null || _d === void 0 ? void 0 : _d.stack,
        }, true);
    }
    trackErrors(options) {
        if (this.errorListenerExists || !this.canTrack()) {
            return defaultActions;
        }
        this.errorsOptions = options;
        window.addEventListener('error', this.captureError);
        this.errorListenerExists = true;
        return {
            stop: () => {
                window.removeEventListener('error', this.captureError);
                this.errorListenerExists = false;
            },
        };
    }
    submitError(payload, evokeCallback) {
        var _a, _b, _c;
        const privateData = {
            pid: this.projectID,
        };
        const errorPayload = {
            pg: this.activePage ||
                getPath({
                    hash: (_a = this.pageViewsOptions) === null || _a === void 0 ? void 0 : _a.hash,
                    search: (_b = this.pageViewsOptions) === null || _b === void 0 ? void 0 : _b.search,
                }),
            lc: getLocale(),
            tz: getTimezone(),
            ...payload,
        };
        if (evokeCallback && ((_c = this.errorsOptions) === null || _c === void 0 ? void 0 : _c.callback)) {
            const callbackResult = this.errorsOptions.callback(errorPayload);
            if (callbackResult === false) {
                return;
            }
            if (callbackResult && typeof callbackResult === 'object') {
                Object.assign(errorPayload, callbackResult);
            }
        }
        Object.assign(errorPayload, privateData);
        this.sendRequest('error', errorPayload);
    }
    async track(event) {
        var _a, _b, _c, _d;
        if (!this.canTrack()) {
            return;
        }
        const data = {
            ...event,
            pid: this.projectID,
            pg: this.activePage ||
                getPath({
                    hash: (_a = this.pageViewsOptions) === null || _a === void 0 ? void 0 : _a.hash,
                    search: (_b = this.pageViewsOptions) === null || _b === void 0 ? void 0 : _b.search,
                }),
            lc: getLocale(),
            tz: getTimezone(),
            ref: getReferrer(),
            so: getUTMSource(),
            me: getUTMMedium(),
            ca: getUTMCampaign(),
            te: getUTMTerm(),
            co: getUTMContent(),
            profileId: (_c = event.profileId) !== null && _c !== void 0 ? _c : (_d = this.options) === null || _d === void 0 ? void 0 : _d.profileId,
        };
        await this.sendRequest('custom', data);
    }
    trackPageViews(options) {
        if (!this.canTrack()) {
            return defaultActions;
        }
        if (this.pageData) {
            return this.pageData.actions;
        }
        this.pageViewsOptions = options;
        let interval;
        if (!(options === null || options === void 0 ? void 0 : options.unique)) {
            interval = setInterval(this.trackPathChange, 2000);
        }
        setTimeout(this.heartbeat, 3000);
        const hbInterval = setInterval(this.heartbeat, 28000);
        const path = getPath({
            hash: options === null || options === void 0 ? void 0 : options.hash,
            search: options === null || options === void 0 ? void 0 : options.search,
        });
        this.pageData = {
            path,
            actions: {
                stop: () => {
                    clearInterval(interval);
                    clearInterval(hbInterval);
                },
            },
        };
        this.trackPage(path, options === null || options === void 0 ? void 0 : options.unique);
        return this.pageData.actions;
    }
    getPerformanceStats() {
        var _a;
        if (!this.canTrack() || this.perfStatsCollected || !((_a = window.performance) === null || _a === void 0 ? void 0 : _a.getEntriesByType)) {
            return {};
        }
        const perf = window.performance.getEntriesByType('navigation')[0];
        if (!perf) {
            return {};
        }
        this.perfStatsCollected = true;
        return {
            // Network
            dns: perf.domainLookupEnd - perf.domainLookupStart, // DNS Resolution
            tls: perf.secureConnectionStart ? perf.requestStart - perf.secureConnectionStart : 0, // TLS Setup; checking if secureConnectionStart is not 0 (it's 0 for non-https websites)
            conn: perf.secureConnectionStart
                ? perf.secureConnectionStart - perf.connectStart
                : perf.connectEnd - perf.connectStart, // Connection time
            response: perf.responseEnd - perf.responseStart, // Response Time (Download)
            // Frontend
            render: perf.domComplete - perf.domContentLoadedEventEnd, // Browser rendering the HTML time
            dom_load: perf.domContentLoadedEventEnd - perf.responseEnd, // DOM loading timing
            page_load: perf.loadEventStart, // Page load time
            // Backend
            ttfb: perf.responseStart - perf.requestStart,
        };
    }
    /**
     * Fetches all feature flags and experiments for the project.
     * Results are cached for 5 minutes by default.
     *
     * @param options - Options for evaluating feature flags.
     * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
     * @returns A promise that resolves to a record of flag keys to boolean values.
     */
    async getFeatureFlags(options, forceRefresh) {
        var _a, _b, _c, _d;
        if (!isInBrowser()) {
            return {};
        }
        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;
        // Check cache first - must match profileId and not be expired
        if (!forceRefresh && this.cachedData) {
            const now = Date.now();
            const isSameProfile = this.cachedData.profileId === requestedProfileId;
            if (isSameProfile && now - this.cachedData.timestamp < DEFAULT_CACHE_DURATION) {
                return this.cachedData.flags;
            }
        }
        try {
            await this.fetchFlagsAndExperiments(options);
            return ((_c = this.cachedData) === null || _c === void 0 ? void 0 : _c.flags) || {};
        }
        catch (error) {
            console.warn('[Swetrix] Error fetching feature flags:', error);
            return ((_d = this.cachedData) === null || _d === void 0 ? void 0 : _d.flags) || {};
        }
    }
    /**
     * Internal method to fetch both feature flags and experiments from the API.
     */
    async fetchFlagsAndExperiments(options) {
        var _a, _b, _c, _d;
        const apiBase = this.getApiBase();
        const body = {
            pid: this.projectID,
        };
        // Use profileId from options, or fall back to global profileId
        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;
        if (profileId) {
            body.profileId = profileId;
        }
        const response = await fetch(`${apiBase}/feature-flag/evaluate`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(body),
        });
        if (!response.ok) {
            console.warn('[Swetrix] Failed to fetch feature flags and experiments:', response.status);
            return;
        }
        const data = (await response.json());
        // Use profileId from options, or fall back to global profileId
        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;
        // Update cache with both flags and experiments
        this.cachedData = {
            flags: data.flags || {},
            experiments: data.experiments || {},
            timestamp: Date.now(),
            profileId: cachedProfileId,
        };
    }
    /**
     * Gets the value of a single feature flag.
     *
     * @param key - The feature flag key.
     * @param options - Options for evaluating the feature flag.
     * @param defaultValue - Default value to return if the flag is not found. Defaults to false.
     * @returns A promise that resolves to the boolean value of the flag.
     */
    async getFeatureFlag(key, options, defaultValue = false) {
        var _a;
        const flags = await this.getFeatureFlags(options);
        return (_a = flags[key]) !== null && _a !== void 0 ? _a : defaultValue;
    }
    /**
     * Clears the cached feature flags and experiments, forcing a fresh fetch on the next call.
     */
    clearFeatureFlagsCache() {
        this.cachedData = null;
    }
    /**
     * Fetches all A/B test experiments for the project.
     * Results are cached for 5 minutes by default (shared cache with feature flags).
     *
     * @param options - Options for evaluating experiments.
     * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
     * @returns A promise that resolves to a record of experiment IDs to variant keys.
     *
     * @example
     * ```typescript
     * const experiments = await getExperiments()
     * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }
     * ```
     */
    async getExperiments(options, forceRefresh) {
        var _a, _b, _c, _d;
        if (!isInBrowser()) {
            return {};
        }
        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;
        // Check cache first - must match profileId and not be expired
        if (!forceRefresh && this.cachedData) {
            const now = Date.now();
            const isSameProfile = this.cachedData.profileId === requestedProfileId;
            if (isSameProfile && now - this.cachedData.timestamp < DEFAULT_CACHE_DURATION) {
                return this.cachedData.experiments;
            }
        }
        try {
            await this.fetchFlagsAndExperiments(options);
            return ((_c = this.cachedData) === null || _c === void 0 ? void 0 : _c.experiments) || {};
        }
        catch (error) {
            console.warn('[Swetrix] Error fetching experiments:', error);
            return ((_d = this.cachedData) === null || _d === void 0 ? void 0 : _d.experiments) || {};
        }
    }
    /**
     * Gets the variant key for a specific A/B test experiment.
     *
     * @param experimentId - The experiment ID.
     * @param options - Options for evaluating the experiment.
     * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.
     * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.
     *
     * @example
     * ```typescript
     * const variant = await getExperiment('checkout-redesign')
     *
     * if (variant === 'new-checkout') {
     *   // Show new checkout flow
     * } else {
     *   // Show control (original) checkout
     * }
     * ```
     */
    async getExperiment(experimentId, options, defaultVariant = null) {
        var _a;
        const experiments = await this.getExperiments(options);
        return (_a = experiments[experimentId]) !== null && _a !== void 0 ? _a : defaultVariant;
    }
    /**
     * Clears the cached experiments (alias for clearFeatureFlagsCache since they share the same cache).
     */
    clearExperimentsCache() {
        this.cachedData = null;
    }
    /**
     * Gets the anonymous profile ID for the current visitor.
     * If profileId was set via init options, returns that.
     * Otherwise, requests server to generate one from IP/UA hash.
     *
     * This ID can be used for revenue attribution with payment providers.
     *
     * @returns A promise that resolves to the profile ID string, or null on error.
     *
     * @example
     * ```typescript
     * const profileId = await swetrix.getProfileId()
     *
     * // Pass to Paddle Checkout for revenue attribution
     * Paddle.Checkout.open({
     *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
     *   customData: {
     *     swetrix_profile_id: profileId,
     *     swetrix_session_id: await swetrix.getSessionId()
     *   }
     * })
     * ```
     */
    async getProfileId() {
        var _a;
        // If profileId is already set in options, return it
        if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.profileId) {
            return this.options.profileId;
        }
        if (!isInBrowser()) {
            return null;
        }
        try {
            const apiBase = this.getApiBase();
            const response = await fetch(`${apiBase}/log/profile-id`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ pid: this.projectID }),
            });
            if (!response.ok) {
                return null;
            }
            const data = (await response.json());
            return data.profileId;
        }
        catch (_b) {
            return null;
        }
    }
    /**
     * Gets the current session ID for the visitor.
     * Session IDs are generated server-side based on IP and user agent.
     *
     * This ID can be used for revenue attribution with payment providers.
     *
     * @returns A promise that resolves to the session ID string, or null on error.
     *
     * @example
     * ```typescript
     * const sessionId = await swetrix.getSessionId()
     *
     * // Pass to Paddle Checkout for revenue attribution
     * Paddle.Checkout.open({
     *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
     *   customData: {
     *     swetrix_profile_id: await swetrix.getProfileId(),
     *     swetrix_session_id: sessionId
     *   }
     * })
     * ```
     */
    async getSessionId() {
        if (!isInBrowser()) {
            return null;
        }
        try {
            const apiBase = this.getApiBase();
            const response = await fetch(`${apiBase}/log/session-id`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ pid: this.projectID }),
            });
            if (!response.ok) {
                return null;
            }
            const data = (await response.json());
            return data.sessionId;
        }
        catch (_a) {
            return null;
        }
    }
    /**
     * Gets the API base URL (without /log suffix).
     */
    getApiBase() {
        var _a;
        if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.apiURL) {
            // Remove trailing /log if present
            return this.options.apiURL.replace(/\/log\/?$/, '');
        }
        return DEFAULT_API_BASE;
    }
    heartbeat() {
        var _a, _b;
        if (!((_a = this.pageViewsOptions) === null || _a === void 0 ? void 0 : _a.heartbeatOnBackground) && document.visibilityState === 'hidden') {
            return;
        }
        const data = {
            pid: this.projectID,
        };
        if ((_b = this.options) === null || _b === void 0 ? void 0 : _b.profileId) {
            data.profileId = this.options.profileId;
        }
        this.sendRequest('hb', data);
    }
    // Tracking path changes. If path changes -> calling this.trackPage method
    trackPathChange() {
        var _a, _b;
        if (!this.pageData)
            return;
        const newPath = getPath({
            hash: (_a = this.pageViewsOptions) === null || _a === void 0 ? void 0 : _a.hash,
            search: (_b = this.pageViewsOptions) === null || _b === void 0 ? void 0 : _b.search,
        });
        const { path } = this.pageData;
        if (path !== newPath) {
            this.trackPage(newPath, false);
        }
    }
    trackPage(pg, unique = false) {
        if (!this.pageData)
            return;
        this.pageData.path = pg;
        const perf = this.getPerformanceStats();
        this.activePage = pg;
        this.submitPageView({ pg }, unique, perf, true);
    }
    submitPageView(payload, unique, perf, evokeCallback) {
        var _a, _b;
        const privateData = {
            pid: this.projectID,
            perf,
            unique,
        };
        const pvPayload = {
            lc: getLocale(),
            tz: getTimezone(),
            ref: getReferrer(),
            so: getUTMSource(),
            me: getUTMMedium(),
            ca: getUTMCampaign(),
            te: getUTMTerm(),
            co: getUTMContent(),
            profileId: (_a = this.options) === null || _a === void 0 ? void 0 : _a.profileId,
            ...payload,
        };
        if (evokeCallback && ((_b = this.pageViewsOptions) === null || _b === void 0 ? void 0 : _b.callback)) {
            const callbackResult = this.pageViewsOptions.callback(pvPayload);
            if (callbackResult === false) {
                return;
            }
            if (callbackResult && typeof callbackResult === 'object') {
                Object.assign(pvPayload, callbackResult);
            }
        }
        Object.assign(pvPayload, privateData);
        this.sendRequest('', pvPayload);
    }
    canTrack() {
        var _a, _b, _c, _d;
        if (((_a = this.options) === null || _a === void 0 ? void 0 : _a.disabled) ||
            !isInBrowser() ||
            (((_b = this.options) === null || _b === void 0 ? void 0 : _b.respectDNT) && ((_c = window.navigator) === null || _c === void 0 ? void 0 : _c.doNotTrack) === '1') ||
            (!((_d = this.options) === null || _d === void 0 ? void 0 : _d.devMode) && isLocalhost()) ||
            isAutomated()) {
            return false;
        }
        return true;
    }
    async sendRequest(path, body) {
        var _a;
        const host = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.apiURL) || DEFAULT_API_HOST;
        await fetch(`${host}/${path}`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(body),
        });
    }
}

exports.LIB_INSTANCE = null;
/**
 * Initialise the tracking library instance (other methods won't work if the library is not initialised).
 *
 * @param {string} pid The Project ID to link the instance of Swetrix.js to.
 * @param {LibOptions} options Options related to the tracking.
 * @returns {Lib} Instance of the Swetrix.js.
 */
function init(pid, options) {
    if (!exports.LIB_INSTANCE) {
        exports.LIB_INSTANCE = new Lib(pid, options);
    }
    return exports.LIB_INSTANCE;
}
/**
 * With this function you are able to track any custom events you want.
 * You should never send any identifiable data (like User ID, email, session cookie, etc.) as an event name.
 * The total number of track calls and their conversion rate will be saved.
 *
 * @param {TrackEventOptions} event The options related to the custom event.
 */
async function track(event) {
    if (!exports.LIB_INSTANCE)
        return;
    await exports.LIB_INSTANCE.track(event);
}
/**
 * With this function you are able to automatically track pageviews across your application.
 *
 * @param {PageViewsOptions} options Pageviews tracking options.
 * @returns {PageActions} The actions related to the tracking. Used to stop tracking pages.
 */
function trackViews(options) {
    return new Promise((resolve) => {
        if (!exports.LIB_INSTANCE) {
            resolve(defaultActions);
            return;
        }
        // We need to verify that document.readyState is complete for the performance stats to be collected correctly.
        if (typeof document === 'undefined' || document.readyState === 'complete') {
            resolve(exports.LIB_INSTANCE.trackPageViews(options));
        }
        else {
            window.addEventListener('load', () => {
                // @ts-ignore
                resolve(exports.LIB_INSTANCE.trackPageViews(options));
            });
        }
    });
}
/**
 * This function is used to set up automatic error events tracking.
 * It set's up an error listener, and whenever an error happens, it gets tracked.
 *
 * @returns {ErrorActions} The actions related to the tracking. Used to stop tracking errors.
 */
function trackErrors(options) {
    if (!exports.LIB_INSTANCE) {
        return defaultActions;
    }
    return exports.LIB_INSTANCE.trackErrors(options);
}
/**
 * This function is used to manually track an error event.
 * It's useful if you want to track specific errors in your application.
 *
 * @param payload Swetrix error object to send.
 * @returns void
 */
function trackError(payload) {
    if (!exports.LIB_INSTANCE)
        return;
    exports.LIB_INSTANCE.submitError(payload, false);
}
/**
 * This function is used to manually track a page view event.
 * It's useful if your application uses esoteric routing which is not supported by Swetrix by default.
 *
 * @deprecated This function is deprecated and will be removed soon, please use the `pageview` instead.
 * @param pg Path of the page to track (this will be sent to the Swetrix API and displayed in the dashboard).
 * @param _prev Path of the previous page (deprecated and ignored).
 * @param unique If set to `true`, only 1 event with the same ID will be saved per user session.
 * @returns void
 */
function trackPageview(pg, _prev, unique) {
    if (!exports.LIB_INSTANCE)
        return;
    exports.LIB_INSTANCE.submitPageView({ pg }, Boolean(unique), {});
}
function pageview(options) {
    if (!exports.LIB_INSTANCE)
        return;
    exports.LIB_INSTANCE.submitPageView(options.payload, Boolean(options.unique), {});
}
/**
 * Fetches all feature flags for the project.
 * Results are cached for 5 minutes by default.
 *
 * @param options - Options for evaluating feature flags (visitorId, attributes).
 * @param forceRefresh - If true, bypasses the cache and fetches fresh flags.
 * @returns A promise that resolves to a record of flag keys to boolean values.
 *
 * @example
 * ```typescript
 * const flags = await getFeatureFlags({
 *   visitorId: 'user-123',
 *   attributes: { cc: 'US', dv: 'desktop' }
 * })
 *
 * if (flags['new-checkout']) {
 *   // Show new checkout flow
 * }
 * ```
 */
async function getFeatureFlags(options, forceRefresh) {
    if (!exports.LIB_INSTANCE)
        return {};
    return exports.LIB_INSTANCE.getFeatureFlags(options, forceRefresh);
}
/**
 * Gets the value of a single feature flag.
 *
 * @param key - The feature flag key.
 * @param options - Options for evaluating the feature flag (visitorId, attributes).
 * @param defaultValue - Default value to return if the flag is not found. Defaults to false.
 * @returns A promise that resolves to the boolean value of the flag.
 *
 * @example
 * ```typescript
 * const isEnabled = await getFeatureFlag('dark-mode', { visitorId: 'user-123' })
 *
 * if (isEnabled) {
 *   // Enable dark mode
 * }
 * ```
 */
async function getFeatureFlag(key, options, defaultValue = false) {
    if (!exports.LIB_INSTANCE)
        return defaultValue;
    return exports.LIB_INSTANCE.getFeatureFlag(key, options, defaultValue);
}
/**
 * Clears the cached feature flags, forcing a fresh fetch on the next call.
 * Useful when you know the user's context has changed significantly.
 */
function clearFeatureFlagsCache() {
    if (!exports.LIB_INSTANCE)
        return;
    exports.LIB_INSTANCE.clearFeatureFlagsCache();
}
/**
 * Fetches all A/B test experiments for the project.
 * Results are cached for 5 minutes by default (shared cache with feature flags).
 *
 * @param options - Options for evaluating experiments.
 * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
 * @returns A promise that resolves to a record of experiment IDs to variant keys.
 *
 * @example
 * ```typescript
 * const experiments = await getExperiments()
 * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }
 *
 * // Use the assigned variant
 * const checkoutVariant = experiments['checkout-experiment-id']
 * if (checkoutVariant === 'new-checkout') {
 *   showNewCheckout()
 * } else {
 *   showOriginalCheckout()
 * }
 * ```
 */
async function getExperiments(options, forceRefresh) {
    if (!exports.LIB_INSTANCE)
        return {};
    return exports.LIB_INSTANCE.getExperiments(options, forceRefresh);
}
/**
 * Gets the variant key for a specific A/B test experiment.
 *
 * @param experimentId - The experiment ID.
 * @param options - Options for evaluating the experiment.
 * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.
 * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.
 *
 * @example
 * ```typescript
 * const variant = await getExperiment('checkout-redesign-experiment-id')
 *
 * if (variant === 'new-checkout') {
 *   // Show new checkout flow
 *   showNewCheckout()
 * } else if (variant === 'control') {
 *   // Show original checkout (control group)
 *   showOriginalCheckout()
 * } else {
 *   // Experiment not running or user not included
 *   showOriginalCheckout()
 * }
 * ```
 */
async function getExperiment(experimentId, options, defaultVariant = null) {
    if (!exports.LIB_INSTANCE)
        return defaultVariant;
    return exports.LIB_INSTANCE.getExperiment(experimentId, options, defaultVariant);
}
/**
 * Clears the cached experiments, forcing a fresh fetch on the next call.
 * This is an alias for clearFeatureFlagsCache since experiments and flags share the same cache.
 */
function clearExperimentsCache() {
    if (!exports.LIB_INSTANCE)
        return;
    exports.LIB_INSTANCE.clearExperimentsCache();
}
/**
 * Gets the anonymous profile ID for the current visitor.
 * If profileId was set via init options, returns that.
 * Otherwise, requests server to generate one from IP/UA hash.
 *
 * This ID can be used for revenue attribution with payment providers like Paddle.
 *
 * @returns A promise that resolves to the profile ID string, or null on error.
 *
 * @example
 * ```typescript
 * const profileId = await getProfileId()
 *
 * // Pass to Paddle Checkout for revenue attribution
 * Paddle.Checkout.open({
 *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
 *   customData: {
 *     swetrix_profile_id: profileId,
 *     swetrix_session_id: await getSessionId()
 *   }
 * })
 * ```
 */
async function getProfileId() {
    if (!exports.LIB_INSTANCE)
        return null;
    return exports.LIB_INSTANCE.getProfileId();
}
/**
 * Gets the current session ID for the visitor.
 * Session IDs are generated server-side based on IP and user agent.
 *
 * This ID can be used for revenue attribution with payment providers like Paddle.
 *
 * @returns A promise that resolves to the session ID string, or null on error.
 *
 * @example
 * ```typescript
 * const sessionId = await getSessionId()
 *
 * // Pass to Paddle Checkout for revenue attribution
 * Paddle.Checkout.open({
 *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
 *   customData: {
 *     swetrix_profile_id: await getProfileId(),
 *     swetrix_session_id: sessionId
 *   }
 * })
 * ```
 */
async function getSessionId() {
    if (!exports.LIB_INSTANCE)
        return null;
    return exports.LIB_INSTANCE.getSessionId();
}

exports.clearExperimentsCache = clearExperimentsCache;
exports.clearFeatureFlagsCache = clearFeatureFlagsCache;
exports.getExperiment = getExperiment;
exports.getExperiments = getExperiments;
exports.getFeatureFlag = getFeatureFlag;
exports.getFeatureFlags = getFeatureFlags;
exports.getProfileId = getProfileId;
exports.getSessionId = getSessionId;
exports.init = init;
exports.pageview = pageview;
exports.track = track;
exports.trackError = trackError;
exports.trackErrors = trackErrors;
exports.trackPageview = trackPageview;
exports.trackViews = trackViews;
//# sourceMappingURL=swetrix.cjs.js.map


================================================
FILE: dist/swetrix.es5.js
================================================
const findInSearch = (exp) => {
    const res = location.search.match(exp);
    return (res && res[2]) || undefined;
};
const utmSourceRegex = /[?&](ref|source|utm_source|gad_source)=([^?&]+)/;
const utmCampaignRegex = /[?&](utm_campaign|gad_campaignid)=([^?&]+)/;
const utmMediumRegex = /[?&](utm_medium)=([^?&]+)/;
const utmTermRegex = /[?&](utm_term)=([^?&]+)/;
const utmContentRegex = /[?&](utm_content)=([^?&]+)/;
const gclidRegex = /[?&](gclid)=([^?&]+)/;
const getGclid = () => {
    return findInSearch(gclidRegex) ? '<gclid>' : undefined;
};
const isInBrowser = () => {
    return typeof window !== 'undefined';
};
const isLocalhost = () => {
    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) === '';
};
const isAutomated = () => {
    return navigator === null || navigator === void 0 ? void 0 : navigator.webdriver;
};
const getLocale = () => {
    return typeof navigator.languages !== 'undefined' ? navigator.languages[0] : navigator.language;
};
const getTimezone = () => {
    try {
        return Intl.DateTimeFormat().resolvedOptions().timeZone;
    }
    catch (e) {
        return;
    }
};
const getReferrer = () => {
    return document.referrer || undefined;
};
const getUTMSource = () => findInSearch(utmSourceRegex);
const getUTMMedium = () => findInSearch(utmMediumRegex) || getGclid();
const getUTMCampaign = () => findInSearch(utmCampaignRegex);
const getUTMTerm = () => findInSearch(utmTermRegex);
const getUTMContent = () => findInSearch(utmContentRegex);
/**
 * Function used to track the current page (path) of the application.
 * Will work in cases where the path looks like:
 * - /path
 * - /#/path
 * - /path?search
 * - /path?search#hash
 * - /path#hash?search
 *
 * @param options - Options for the function.
 * @param options.hash - Whether to trigger on hash change.
 * @param options.search - Whether to trigger on search change.
 * @returns The path of the current page.
 */
const getPath = (options) => {
    let result = location.pathname || '';
    if (options.hash) {
        const hashIndex = location.hash.indexOf('?');
        const hashString = hashIndex > -1 ? location.hash.substring(0, hashIndex) : location.hash;
        result += hashString;
    }
    if (options.search) {
        const hashIndex = location.hash.indexOf('?');
        const searchString = location.search || (hashIndex > -1 ? location.hash.substring(hashIndex) : '');
        result += searchString;
    }
    return result;
};

const defaultActions = {
    stop() { },
};
const DEFAULT_API_HOST = 'https://api.swetrix.com/log';
const DEFAULT_API_BASE = 'https://api.swetrix.com';
// Default cache duration: 5 minutes
const DEFAULT_CACHE_DURATION = 5 * 60 * 1000;
class Lib {
    constructor(projectID, options) {
        this.projectID = projectID;
        this.options = options;
        this.pageData = null;
        this.pageViewsOptions = null;
        this.errorsOptions = null;
        this.perfStatsCollected = false;
        this.activePage = null;
        this.errorListenerExists = false;
        this.cachedData = null;
        this.trackPathChange = this.trackPathChange.bind(this);
        this.heartbeat = this.heartbeat.bind(this);
        this.captureError = this.captureError.bind(this);
    }
    captureError(event) {
        var _a, _b, _c, _d;
        if (typeof ((_a = this.errorsOptions) === null || _a === void 0 ? void 0 : _a.sampleRate) === 'number' && this.errorsOptions.sampleRate >= Math.random()) {
            return;
        }
        this.submitError({
            // The file in which error occured.
            filename: event.filename,
            // The line of code error occured on.
            lineno: event.lineno,
            // The column of code error occured on.
            colno: event.colno,
            // 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.
            name: ((_b = event.error) === null || _b === void 0 ? void 0 : _b.name) || 'Error',
            // Description of the error. By default, we use message from Error object, is it does not contain the error name
            // (we want to split error name and message so we could group them together later in dashboard).
            // If message in error object does not exist - lets use a message from the Error event itself.
            message: ((_c = event.error) === null || _c === void 0 ? void 0 : _c.message) || event.message,
            // Stack trace of the error, if available.
            stackTrace: (_d = event.error) === null || _d === void 0 ? void 0 : _d.stack,
        }, true);
    }
    trackErrors(options) {
        if (this.errorListenerExists || !this.canTrack()) {
            return defaultActions;
        }
        this.errorsOptions = options;
        window.addEventListener('error', this.captureError);
        this.errorListenerExists = true;
        return {
            stop: () => {
                window.removeEventListener('error', this.captureError);
                this.errorListenerExists = false;
            },
        };
    }
    submitError(payload, evokeCallback) {
        var _a, _b, _c;
        const privateData = {
            pid: this.projectID,
        };
        const errorPayload = {
            pg: this.activePage ||
                getPath({
                    hash: (_a = this.pageViewsOptions) === null || _a === void 0 ? void 0 : _a.hash,
                    search: (_b = this.pageViewsOptions) === null || _b === void 0 ? void 0 : _b.search,
                }),
            lc: getLocale(),
            tz: getTimezone(),
            ...payload,
        };
        if (evokeCallback && ((_c = this.errorsOptions) === null || _c === void 0 ? void 0 : _c.callback)) {
            const callbackResult = this.errorsOptions.callback(errorPayload);
            if (callbackResult === false) {
                return;
            }
            if (callbackResult && typeof callbackResult === 'object') {
                Object.assign(errorPayload, callbackResult);
            }
        }
        Object.assign(errorPayload, privateData);
        this.sendRequest('error', errorPayload);
    }
    async track(event) {
        var _a, _b, _c, _d;
        if (!this.canTrack()) {
            return;
        }
        const data = {
            ...event,
            pid: this.projectID,
            pg: this.activePage ||
                getPath({
                    hash: (_a = this.pageViewsOptions) === null || _a === void 0 ? void 0 : _a.hash,
                    search: (_b = this.pageViewsOptions) === null || _b === void 0 ? void 0 : _b.search,
                }),
            lc: getLocale(),
            tz: getTimezone(),
            ref: getReferrer(),
            so: getUTMSource(),
            me: getUTMMedium(),
            ca: getUTMCampaign(),
            te: getUTMTerm(),
            co: getUTMContent(),
            profileId: (_c = event.profileId) !== null && _c !== void 0 ? _c : (_d = this.options) === null || _d === void 0 ? void 0 : _d.profileId,
        };
        await this.sendRequest('custom', data);
    }
    trackPageViews(options) {
        if (!this.canTrack()) {
            return defaultActions;
        }
        if (this.pageData) {
            return this.pageData.actions;
        }
        this.pageViewsOptions = options;
        let interval;
        if (!(options === null || options === void 0 ? void 0 : options.unique)) {
            interval = setInterval(this.trackPathChange, 2000);
        }
        setTimeout(this.heartbeat, 3000);
        const hbInterval = setInterval(this.heartbeat, 28000);
        const path = getPath({
            hash: options === null || options === void 0 ? void 0 : options.hash,
            search: options === null || options === void 0 ? void 0 : options.search,
        });
        this.pageData = {
            path,
            actions: {
                stop: () => {
                    clearInterval(interval);
                    clearInterval(hbInterval);
                },
            },
        };
        this.trackPage(path, options === null || options === void 0 ? void 0 : options.unique);
        return this.pageData.actions;
    }
    getPerformanceStats() {
        var _a;
        if (!this.canTrack() || this.perfStatsCollected || !((_a = window.performance) === null || _a === void 0 ? void 0 : _a.getEntriesByType)) {
            return {};
        }
        const perf = window.performance.getEntriesByType('navigation')[0];
        if (!perf) {
            return {};
        }
        this.perfStatsCollected = true;
        return {
            // Network
            dns: perf.domainLookupEnd - perf.domainLookupStart, // DNS Resolution
            tls: perf.secureConnectionStart ? perf.requestStart - perf.secureConnectionStart : 0, // TLS Setup; checking if secureConnectionStart is not 0 (it's 0 for non-https websites)
            conn: perf.secureConnectionStart
                ? perf.secureConnectionStart - perf.connectStart
                : perf.connectEnd - perf.connectStart, // Connection time
            response: perf.responseEnd - perf.responseStart, // Response Time (Download)
            // Frontend
            render: perf.domComplete - perf.domContentLoadedEventEnd, // Browser rendering the HTML time
            dom_load: perf.domContentLoadedEventEnd - perf.responseEnd, // DOM loading timing
            page_load: perf.loadEventStart, // Page load time
            // Backend
            ttfb: perf.responseStart - perf.requestStart,
        };
    }
    /**
     * Fetches all feature flags and experiments for the project.
     * Results are cached for 5 minutes by default.
     *
     * @param options - Options for evaluating feature flags.
     * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
     * @returns A promise that resolves to a record of flag keys to boolean values.
     */
    async getFeatureFlags(options, forceRefresh) {
        var _a, _b, _c, _d;
        if (!isInBrowser()) {
            return {};
        }
        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;
        // Check cache first - must match profileId and not be expired
        if (!forceRefresh && this.cachedData) {
            const now = Date.now();
            const isSameProfile = this.cachedData.profileId === requestedProfileId;
            if (isSameProfile && now - this.cachedData.timestamp < DEFAULT_CACHE_DURATION) {
                return this.cachedData.flags;
            }
        }
        try {
            await this.fetchFlagsAndExperiments(options);
            return ((_c = this.cachedData) === null || _c === void 0 ? void 0 : _c.flags) || {};
        }
        catch (error) {
            console.warn('[Swetrix] Error fetching feature flags:', error);
            return ((_d = this.cachedData) === null || _d === void 0 ? void 0 : _d.flags) || {};
        }
    }
    /**
     * Internal method to fetch both feature flags and experiments from the API.
     */
    async fetchFlagsAndExperiments(options) {
        var _a, _b, _c, _d;
        const apiBase = this.getApiBase();
        const body = {
            pid: this.projectID,
        };
        // Use profileId from options, or fall back to global profileId
        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;
        if (profileId) {
            body.profileId = profileId;
        }
        const response = await fetch(`${apiBase}/feature-flag/evaluate`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(body),
        });
        if (!response.ok) {
            console.warn('[Swetrix] Failed to fetch feature flags and experiments:', response.status);
            return;
        }
        const data = (await response.json());
        // Use profileId from options, or fall back to global profileId
        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;
        // Update cache with both flags and experiments
        this.cachedData = {
            flags: data.flags || {},
            experiments: data.experiments || {},
            timestamp: Date.now(),
            profileId: cachedProfileId,
        };
    }
    /**
     * Gets the value of a single feature flag.
     *
     * @param key - The feature flag key.
     * @param options - Options for evaluating the feature flag.
     * @param defaultValue - Default value to return if the flag is not found. Defaults to false.
     * @returns A promise that resolves to the boolean value of the flag.
     */
    async getFeatureFlag(key, options, defaultValue = false) {
        var _a;
        const flags = await this.getFeatureFlags(options);
        return (_a = flags[key]) !== null && _a !== void 0 ? _a : defaultValue;
    }
    /**
     * Clears the cached feature flags and experiments, forcing a fresh fetch on the next call.
     */
    clearFeatureFlagsCache() {
        this.cachedData = null;
    }
    /**
     * Fetches all A/B test experiments for the project.
     * Results are cached for 5 minutes by default (shared cache with feature flags).
     *
     * @param options - Options for evaluating experiments.
     * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
     * @returns A promise that resolves to a record of experiment IDs to variant keys.
     *
     * @example
     * ```typescript
     * const experiments = await getExperiments()
     * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }
     * ```
     */
    async getExperiments(options, forceRefresh) {
        var _a, _b, _c, _d;
        if (!isInBrowser()) {
            return {};
        }
        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;
        // Check cache first - must match profileId and not be expired
        if (!forceRefresh && this.cachedData) {
            const now = Date.now();
            const isSameProfile = this.cachedData.profileId === requestedProfileId;
            if (isSameProfile && now - this.cachedData.timestamp < DEFAULT_CACHE_DURATION) {
                return this.cachedData.experiments;
            }
        }
        try {
            await this.fetchFlagsAndExperiments(options);
            return ((_c = this.cachedData) === null || _c === void 0 ? void 0 : _c.experiments) || {};
        }
        catch (error) {
            console.warn('[Swetrix] Error fetching experiments:', error);
            return ((_d = this.cachedData) === null || _d === void 0 ? void 0 : _d.experiments) || {};
        }
    }
    /**
     * Gets the variant key for a specific A/B test experiment.
     *
     * @param experimentId - The experiment ID.
     * @param options - Options for evaluating the experiment.
     * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.
     * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.
     *
     * @example
     * ```typescript
     * const variant = await getExperiment('checkout-redesign')
     *
     * if (variant === 'new-checkout') {
     *   // Show new checkout flow
     * } else {
     *   // Show control (original) checkout
     * }
     * ```
     */
    async getExperiment(experimentId, options, defaultVariant = null) {
        var _a;
        const experiments = await this.getExperiments(options);
        return (_a = experiments[experimentId]) !== null && _a !== void 0 ? _a : defaultVariant;
    }
    /**
     * Clears the cached experiments (alias for clearFeatureFlagsCache since they share the same cache).
     */
    clearExperimentsCache() {
        this.cachedData = null;
    }
    /**
     * Gets the anonymous profile ID for the current visitor.
     * If profileId was set via init options, returns that.
     * Otherwise, requests server to generate one from IP/UA hash.
     *
     * This ID can be used for revenue attribution with payment providers.
     *
     * @returns A promise that resolves to the profile ID string, or null on error.
     *
     * @example
     * ```typescript
     * const profileId = await swetrix.getProfileId()
     *
     * // Pass to Paddle Checkout for revenue attribution
     * Paddle.Checkout.open({
     *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
     *   customData: {
     *     swetrix_profile_id: profileId,
     *     swetrix_session_id: await swetrix.getSessionId()
     *   }
     * })
     * ```
     */
    async getProfileId() {
        var _a;
        // If profileId is already set in options, return it
        if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.profileId) {
            return this.options.profileId;
        }
        if (!isInBrowser()) {
            return null;
        }
        try {
            const apiBase = this.getApiBase();
            const response = await fetch(`${apiBase}/log/profile-id`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ pid: this.projectID }),
            });
            if (!response.ok) {
                return null;
            }
            const data = (await response.json());
            return data.profileId;
        }
        catch (_b) {
            return null;
        }
    }
    /**
     * Gets the current session ID for the visitor.
     * Session IDs are generated server-side based on IP and user agent.
     *
     * This ID can be used for revenue attribution with payment providers.
     *
     * @returns A promise that resolves to the session ID string, or null on error.
     *
     * @example
     * ```typescript
     * const sessionId = await swetrix.getSessionId()
     *
     * // Pass to Paddle Checkout for revenue attribution
     * Paddle.Checkout.open({
     *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
     *   customData: {
     *     swetrix_profile_id: await swetrix.getProfileId(),
     *     swetrix_session_id: sessionId
     *   }
     * })
     * ```
     */
    async getSessionId() {
        if (!isInBrowser()) {
            return null;
        }
        try {
            const apiBase = this.getApiBase();
            const response = await fetch(`${apiBase}/log/session-id`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ pid: this.projectID }),
            });
            if (!response.ok) {
                return null;
            }
            const data = (await response.json());
            return data.sessionId;
        }
        catch (_a) {
            return null;
        }
    }
    /**
     * Gets the API base URL (without /log suffix).
     */
    getApiBase() {
        var _a;
        if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.apiURL) {
            // Remove trailing /log if present
            return this.options.apiURL.replace(/\/log\/?$/, '');
        }
        return DEFAULT_API_BASE;
    }
    heartbeat() {
        var _a, _b;
        if (!((_a = this.pageViewsOptions) === null || _a === void 0 ? void 0 : _a.heartbeatOnBackground) && document.visibilityState === 'hidden') {
            return;
        }
        const data = {
            pid: this.projectID,
        };
        if ((_b = this.options) === null || _b === void 0 ? void 0 : _b.profileId) {
            data.profileId = this.options.profileId;
        }
        this.sendRequest('hb', data);
    }
    // Tracking path changes. If path changes -> calling this.trackPage method
    trackPathChange() {
        var _a, _b;
        if (!this.pageData)
            return;
        const newPath = getPath({
            hash: (_a = this.pageViewsOptions) === null || _a === void 0 ? void 0 : _a.hash,
            search: (_b = this.pageViewsOptions) === null || _b === void 0 ? void 0 : _b.search,
        });
        const { path } = this.pageData;
        if (path !== newPath) {
            this.trackPage(newPath, false);
        }
    }
    trackPage(pg, unique = false) {
        if (!this.pageData)
            return;
        this.pageData.path = pg;
        const perf = this.getPerformanceStats();
        this.activePage = pg;
        this.submitPageView({ pg }, unique, perf, true);
    }
    submitPageView(payload, unique, perf, evokeCallback) {
        var _a, _b;
        const privateData = {
            pid: this.projectID,
            perf,
            unique,
        };
        const pvPayload = {
            lc: getLocale(),
            tz: getTimezone(),
            ref: getReferrer(),
            so: getUTMSource(),
            me: getUTMMedium(),
            ca: getUTMCampaign(),
            te: getUTMTerm(),
            co: getUTMContent(),
            profileId: (_a = this.options) === null || _a === void 0 ? void 0 : _a.profileId,
            ...payload,
        };
        if (evokeCallback && ((_b = this.pageViewsOptions) === null || _b === void 0 ? void 0 : _b.callback)) {
            const callbackResult = this.pageViewsOptions.callback(pvPayload);
            if (callbackResult === false) {
                return;
            }
            if (callbackResult && typeof callbackResult === 'object') {
                Object.assign(pvPayload, callbackResult);
            }
        }
        Object.assign(pvPayload, privateData);
        this.sendRequest('', pvPayload);
    }
    canTrack() {
        var _a, _b, _c, _d;
        if (((_a = this.options) === null || _a === void 0 ? void 0 : _a.disabled) ||
            !isInBrowser() ||
            (((_b = this.options) === null || _b === void 0 ? void 0 : _b.respectDNT) && ((_c = window.navigator) === null || _c === void 0 ? void 0 : _c.doNotTrack) === '1') ||
            (!((_d = this.options) === null || _d === void 0 ? void 0 : _d.devMode) && isLocalhost()) ||
            isAutomated()) {
            return false;
        }
        return true;
    }
    async sendRequest(path, body) {
        var _a;
        const host = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.apiURL) || DEFAULT_API_HOST;
        await fetch(`${host}/${path}`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(body),
        });
    }
}

let LIB_INSTANCE = null;
/**
 * Initialise the tracking library instance (other methods won't work if the library is not initialised).
 *
 * @param {string} pid The Project ID to link the instance of Swetrix.js to.
 * @param {LibOptions} options Options related to the tracking.
 * @returns {Lib} Instance of the Swetrix.js.
 */
function init(pid, options) {
    if (!LIB_INSTANCE) {
        LIB_INSTANCE = new Lib(pid, options);
    }
    return LIB_INSTANCE;
}
/**
 * With this function you are able to track any custom events you want.
 * You should never send any identifiable data (like User ID, email, session cookie, etc.) as an event name.
 * The total number of track calls and their conversion rate will be saved.
 *
 * @param {TrackEventOptions} event The options related to the custom event.
 */
async function track(event) {
    if (!LIB_INSTANCE)
        return;
    await LIB_INSTANCE.track(event);
}
/**
 * With this function you are able to automatically track pageviews across your application.
 *
 * @param {PageViewsOptions} options Pageviews tracking options.
 * @returns {PageActions} The actions related to the tracking. Used to stop tracking pages.
 */
function trackViews(options) {
    return new Promise((resolve) => {
        if (!LIB_INSTANCE) {
            resolve(defaultActions);
            return;
        }
        // We need to verify that document.readyState is complete for the performance stats to be collected correctly.
        if (typeof document === 'undefined' || document.readyState === 'complete') {
            resolve(LIB_INSTANCE.trackPageViews(options));
        }
        else {
            window.addEventListener('load', () => {
                // @ts-ignore
                resolve(LIB_INSTANCE.trackPageViews(options));
            });
        }
    });
}
/**
 * This function is used to set up automatic error events tracking.
 * It set's up an error listener, and whenever an error happens, it gets tracked.
 *
 * @returns {ErrorActions} The actions related to the tracking. Used to stop tracking errors.
 */
function trackErrors(options) {
    if (!LIB_INSTANCE) {
        return defaultActions;
    }
    return LIB_INSTANCE.trackErrors(options);
}
/**
 * This function is used to manually track an error event.
 * It's useful if you want to track specific errors in your application.
 *
 * @param payload Swetrix error object to send.
 * @returns void
 */
function trackError(payload) {
    if (!LIB_INSTANCE)
        return;
    LIB_INSTANCE.submitError(payload, false);
}
/**
 * This function is used to manually track a page view event.
 * It's useful if your application uses esoteric routing which is not supported by Swetrix by default.
 *
 * @deprecated This function is deprecated and will be removed soon, please use the `pageview` instead.
 * @param pg Path of the page to track (this will be sent to the Swetrix API and displayed in the dashboard).
 * @param _prev Path of the previous page (deprecated and ignored).
 * @param unique If set to `true`, only 1 event with the same ID will be saved per user session.
 * @returns void
 */
function trackPageview(pg, _prev, unique) {
    if (!LIB_INSTANCE)
        return;
    LIB_INSTANCE.submitPageView({ pg }, Boolean(unique), {});
}
function pageview(options) {
    if (!LIB_INSTANCE)
        return;
    LIB_INSTANCE.submitPageView(options.payload, Boolean(options.unique), {});
}
/**
 * Fetches all feature flags for the project.
 * Results are cached for 5 minutes by default.
 *
 * @param options - Options for evaluating feature flags (visitorId, attributes).
 * @param forceRefresh - If true, bypasses the cache and fetches fresh flags.
 * @returns A promise that resolves to a record of flag keys to boolean values.
 *
 * @example
 * ```typescript
 * const flags = await getFeatureFlags({
 *   visitorId: 'user-123',
 *   attributes: { cc: 'US', dv: 'desktop' }
 * })
 *
 * if (flags['new-checkout']) {
 *   // Show new checkout flow
 * }
 * ```
 */
async function getFeatureFlags(options, forceRefresh) {
    if (!LIB_INSTANCE)
        return {};
    return LIB_INSTANCE.getFeatureFlags(options, forceRefresh);
}
/**
 * Gets the value of a single feature flag.
 *
 * @param key - The feature flag key.
 * @param options - Options for evaluating the feature flag (visitorId, attributes).
 * @param defaultValue - Default value to return if the flag is not found. Defaults to false.
 * @returns A promise that resolves to the boolean value of the flag.
 *
 * @example
 * ```typescript
 * const isEnabled = await getFeatureFlag('dark-mode', { visitorId: 'user-123' })
 *
 * if (isEnabled) {
 *   // Enable dark mode
 * }
 * ```
 */
async function getFeatureFlag(key, options, defaultValue = false) {
    if (!LIB_INSTANCE)
        return defaultValue;
    return LIB_INSTANCE.getFeatureFlag(key, options, defaultValue);
}
/**
 * Clears the cached feature flags, forcing a fresh fetch on the next call.
 * Useful when you know the user's context has changed significantly.
 */
function clearFeatureFlagsCache() {
    if (!LIB_INSTANCE)
        return;
    LIB_INSTANCE.clearFeatureFlagsCache();
}
/**
 * Fetches all A/B test experiments for the project.
 * Results are cached for 5 minutes by default (shared cache with feature flags).
 *
 * @param options - Options for evaluating experiments.
 * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
 * @returns A promise that resolves to a record of experiment IDs to variant keys.
 *
 * @example
 * ```typescript
 * const experiments = await getExperiments()
 * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }
 *
 * // Use the assigned variant
 * const checkoutVariant = experiments['checkout-experiment-id']
 * if (checkoutVariant === 'new-checkout') {
 *   showNewCheckout()
 * } else {
 *   showOriginalCheckout()
 * }
 * ```
 */
async function getExperiments(options, forceRefresh) {
    if (!LIB_INSTANCE)
        return {};
    return LIB_INSTANCE.getExperiments(options, forceRefresh);
}
/**
 * Gets the variant key for a specific A/B test experiment.
 *
 * @param experimentId - The experiment ID.
 * @param options - Options for evaluating the experiment.
 * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.
 * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.
 *
 * @example
 * ```typescript
 * const variant = await getExperiment('checkout-redesign-experiment-id')
 *
 * if (variant === 'new-checkout') {
 *   // Show new checkout flow
 *   showNewCheckout()
 * } else if (variant === 'control') {
 *   // Show original checkout (control group)
 *   showOriginalCheckout()
 * } else {
 *   // Experiment not running or user not included
 *   showOriginalCheckout()
 * }
 * ```
 */
async function getExperiment(experimentId, options, defaultVariant = null) {
    if (!LIB_INSTANCE)
        return defaultVariant;
    return LIB_INSTANCE.getExperiment(experimentId, options, defaultVariant);
}
/**
 * Clears the cached experiments, forcing a fresh fetch on the next call.
 * This is an alias for clearFeatureFlagsCache since experiments and flags share the same cache.
 */
function clearExperimentsCache() {
    if (!LIB_INSTANCE)
        return;
    LIB_INSTANCE.clearExperimentsCache();
}
/**
 * Gets the anonymous profile ID for the current visitor.
 * If profileId was set via init options, returns that.
 * Otherwise, requests server to generate one from IP/UA hash.
 *
 * This ID can be used for revenue attribution with payment providers like Paddle.
 *
 * @returns A promise that resolves to the profile ID string, or null on error.
 *
 * @example
 * ```typescript
 * const profileId = await getProfileId()
 *
 * // Pass to Paddle Checkout for revenue attribution
 * Paddle.Checkout.open({
 *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
 *   customData: {
 *     swetrix_profile_id: profileId,
 *     swetrix_session_id: await getSessionId()
 *   }
 * })
 * ```
 */
async function getProfileId() {
    if (!LIB_INSTANCE)
        return null;
    return LIB_INSTANCE.getProfileId();
}
/**
 * Gets the current session ID for the visitor.
 * Session IDs are generated server-side based on IP and user agent.
 *
 * This ID can be used for revenue attribution with payment providers like Paddle.
 *
 * @returns A promise that resolves to the session ID string, or null on error.
 *
 * @example
 * ```typescript
 * const sessionId = await getSessionId()
 *
 * // Pass to Paddle Checkout for revenue attribution
 * Paddle.Checkout.open({
 *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
 *   customData: {
 *     swetrix_profile_id: await getProfileId(),
 *     swetrix_session_id: sessionId
 *   }
 * })
 * ```
 */
async function getSessionId() {
    if (!LIB_INSTANCE)
        return null;
    return LIB_INSTANCE.getSessionId();
}

export { LIB_INSTANCE, clearExperimentsCache, clearFeatureFlagsCache, getExperiment, getExperiments, getFeatureFlag, getFeatureFlags, getProfileId, getSessionId, init, pageview, track, trackError, trackErrors, trackPageview, trackViews };
//# sourceMappingURL=swetrix.es5.js.map


================================================
FILE: dist/swetrix.js
================================================
!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)}))}}));
//# sourceMappingURL=swetrix.js.map


================================================
FILE: jest.config.js
================================================
export default {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  extensionsToTreatAsEsm: ['.ts'],
  moduleNameMapper: {
    '^(\\.{1,2}/.*)\\.js$': '$1',
  },
  transform: {
    '^.+\\.tsx?$': [
      'ts-jest',
      {
        useESM: true,
      },
    ],
  },
}


================================================
FILE: package.json
================================================
{
  "name": "swetrix",
  "version": "4.1.0",
  "description": "The JavaScript analytics client for Swetrix Analytics",
  "type": "module",
  "main": "dist/swetrix.cjs.js",
  "module": "dist/swetrix.es5.js",
  "browser": "dist/swetrix.js",
  "esnext": "dist/esnext/index.js",
  "typings": "dist/esnext/index.d.ts",
  "scripts": {
    "prebuild": "rimraf dist",
    "prepublish": "npm run build",
    "build": "rollup -c && npm run tsc",
    "start": "rollup -c -w",
    "tsc": "tsc -p tsconfig.esnext.json",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  },
  "exports": {
    ".": {
      "import": "./dist/esnext/index.js",
      "require": "./dist/swetrix.cjs.js",
      "types": "./dist/esnext/index.d.ts",
      "default": "./dist/swetrix.js"
    }
  },
  "keywords": [
    "swetrix",
    "analytics",
    "monitoring",
    "metrics",
    "privacy"
  ],
  "repository": {
    "type": "git",
    "url": "git+https://github.com/Swetrix/swetrix-js.git"
  },
  "author": "Andrii Romasiun, Swetrix Ltd. <contact@swetrix.com>",
  "funding": "https://github.com/sponsors/Swetrix",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/Swetrix/swetrix-js/issues"
  },
  "homepage": "https://docs.swetrix.com",
  "devDependencies": {
    "@rollup/plugin-commonjs": "^28.0.3",
    "@rollup/plugin-node-resolve": "^16.0.1",
    "@rollup/plugin-terser": "^0.4.4",
    "@rollup/plugin-typescript": "^12.1.2",
    "@types/jest": "^29.5.14",
    "@types/node": "^22.15.21",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "rimraf": "^6.0.1",
    "rollup": "^4.41.0",
    "tslib": "^2.6.3",
    "ts-jest": "^29.3.4",
    "typescript": "^5.8.3"
  },
  "devEngines": {
    "runtime": {
      "name": "node",
      "onFail": "error",
      "version": ">=22"
    }
  }
}


================================================
FILE: rollup.config.mjs
================================================
import { nodeResolve } from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import typescript from '@rollup/plugin-typescript'
import terser from '@rollup/plugin-terser'
import pkg from './package.json' with { type: 'json' }
import { createRequire } from 'node:module'

const require = createRequire(import.meta.url)

export default [
  {
    input: 'src/index.ts',
    output: [
      { file: pkg.main, format: 'cjs', sourcemap: true },
      { file: pkg.module, format: 'es', sourcemap: true },
    ],
    plugins: [
      typescript({
        outDir: './dist',
        sourceMap: true,
        tslib: require.resolve('tslib'),
      }),
      nodeResolve(),
      commonjs(),
    ],
  },
  {
    input: 'src/index.ts',
    output: [{ file: pkg.browser, format: 'umd', name: 'swetrix', sourcemap: true }],
    plugins: [
      typescript({
        outDir: './dist',
        sourceMap: true,
        tslib: require.resolve('tslib'),
      }),
      nodeResolve(),
      commonjs(),
      terser(),
    ],
  },
]


================================================
FILE: src/Lib.ts
================================================
import {
  isInBrowser,
  isLocalhost,
  isAutomated,
  getLocale,
  getTimezone,
  getReferrer,
  getUTMCampaign,
  getUTMMedium,
  getUTMSource,
  getUTMTerm,
  getUTMContent,
  getPath,
} from './utils.js'

export interface LibOptions {
  /**
   * When set to `true`, localhost events will be sent to server.
   */
  devMode?: boolean

  /**
   * When set to `true`, the tracking library won't send any data to server.
   * Useful for development purposes when this value is set based on `.env` var.
   */
  disabled?: boolean

  /**
   * By setting this flag to `true`, we will not collect ANY kind of data about the user with the DNT setting.
   */
  respectDNT?: boolean

  /** Set a custom URL of the API server (for selfhosted variants of Swetrix). */
  apiURL?: string

  /**
   * Optional profile ID for long-term user tracking.
   * If set, it will be used for all pageviews and events unless overridden per-call.
   */
  profileId?: string
}

export interface TrackEventOptions {
  /** The custom event name. */
  ev: string

  /** If set to `true`, only 1 event with the same ID will be saved per user session. */
  unique?: boolean

  /** Event-related metadata object with string values. */
  meta?: {
    [key: string]: string | number | boolean | null | undefined
  }

  /** Optional profile ID for long-term user tracking. Overrides the global profileId if set. */
  profileId?: string
}

// Partial user-editable pageview payload
export interface IPageViewPayload {
  lc?: string
  tz?: string
  ref?: string
  so?: string
  me?: string
  ca?: string
  te?: string
  co?: string
  pg?: string | null

  /** Pageview-related metadata object with string values. */
  meta?: {
    [key: string]: string | number | boolean | null | undefined
  }

  /** Optional profile ID for long-term user tracking. Overrides the global profileId if set. */
  profileId?: string
}

// Partial user-editable error payload
export interface IErrorEventPayload {
  name: string
  message?: string | null
  lineno?: number | null
  colno?: number | null
  filename?: string | null
  stackTrace?: string | null
  meta?: {
    [key: string]: string | number | boolean | null | undefined
  }
}

export interface IInternalErrorEventPayload extends IErrorEventPayload {
  lc?: string
  tz?: string
  pg?: string | null
}

interface IPerfPayload {
  dns: number
  tls: number
  conn: number
  response: number
  render: number
  dom_load: number
  page_load: number
  ttfb: number
}

/**
 * Options for evaluating feature flags.
 */
export interface FeatureFlagsOptions {
  /**
   * Optional profile ID for long-term user tracking.
   * If not provided, an anonymous profile ID will be generated server-side based on IP and user agent.
   * Overrides the global profileId if set.
   */
  profileId?: string
}

/**
 * Options for evaluating experiments.
 */
export interface ExperimentOptions {
  /**
   * Optional profile ID for long-term user tracking.
   * If not provided, an anonymous profile ID will be generated server-side based on IP and user agent.
   * Overrides the global profileId if set.
   */
  profileId?: string
}

/**
 * Cached feature flags and experiments with timestamp.
 */
interface CachedData {
  flags: Record<string, boolean>
  experiments: Record<string, string>
  timestamp: number
  /** The profileId used when fetching this cached data */
  profileId?: string
}

/**
 * The object returned by `trackPageViews()`, used to stop tracking pages.
 */
export interface PageActions {
  /** Stops the tracking of pages. */
  stop: () => void
}

/**
 * The object returned by `trackErrors()`, used to stop tracking errors.
 */
export interface ErrorActions {
  /** Stops the tracking of errors. */
  stop: () => void
}

export interface PageData {
  /** Current URL path. */
  path: string

  /** The object returned by `trackPageViews()`, used to stop tracking pages. */
  actions: PageActions
}

export interface ErrorOptions {
  /**
   * A number that indicates how many errors should be sent to the server.
   * Accepts values between 0 and 1. For example, if set to 0.5 - only ~50% of errors will be sent to Swetrix.
   * 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.
   *
   * The default value for this option is 1.
   */
  sampleRate?: number

  /**
   * Callback to edit / prevent sending errors.
   *
   * @param payload - The error payload.
   * @returns The edited payload or `false` to prevent sending the error event. If `true` is returned, the payload will be sent as-is.
   */
  callback?: (payload: IInternalErrorEventPayload) => Partial<IInternalErrorEventPayload> | boolean
}

export interface PageViewsOptions {
  /**
   * If set to `true`, only unique events will be saved.
   * This param is useful when tracking single-page landing websites.
   */
  unique?: boolean

  /** Send Heartbeat requests when the website tab is not active in the browser. */
  heartbeatOnBackground?: boolean

  /**
   * Set to `true` to enable hash-based routing.
   * For example if you have pages like /#/path or want to track pages like /path#hash
   */
  hash?: boolean

  /**
   * Set to `true` to enable search-based routing.
   * For example if you have pages like /path?search
   */
  search?: boolean

  /**
   * Callback to edit / prevent sending pageviews.
   *
   * @param payload - The pageview payload.
   * @returns The edited payload or `false` to prevent sending the pageview. If `true` is returned, the payload will be sent as-is.
   */
  callback?: (payload: IPageViewPayload) => Partial<IPageViewPayload> | boolean
}

export const defaultActions = {
  stop() {},
}

const DEFAULT_API_HOST = 'https://api.swetrix.com/log'
const DEFAULT_API_BASE = 'https://api.swetrix.com'

// Default cache duration: 5 minutes
const DEFAULT_CACHE_DURATION = 5 * 60 * 1000

export class Lib {
  private pageData: PageData | null = null
  private pageViewsOptions?: PageViewsOptions | null = null
  private errorsOptions?: ErrorOptions | null = null
  private perfStatsCollected: boolean = false
  private activePage: string | null = null
  private errorListenerExists = false
  private cachedData: CachedData | null = null

  constructor(private projectID: string, private options?: LibOptions) {
    this.trackPathChange = this.trackPathChange.bind(this)
    this.heartbeat = this.heartbeat.bind(this)
    this.captureError = this.captureError.bind(this)
  }

  captureError(event: ErrorEvent): void {
    if (typeof this.errorsOptions?.sampleRate === 'number' && this.errorsOptions.sampleRate >= Math.random()) {
      return
    }

    this.submitError(
      {
        // The file in which error occured.
        filename: event.filename,

        // The line of code error occured on.
        lineno: event.lineno,

        // The column of code error occured on.
        colno: event.colno,

        // 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.
        name: event.error?.name || 'Error',

        // Description of the error. By default, we use message from Error object, is it does not contain the error name
        // (we want to split error name and message so we could group them together later in dashboard).
        // If message in error object does not exist - lets use a message from the Error event itself.
        message: event.error?.message || event.message,

        // Stack trace of the error, if available.
        stackTrace: event.error?.stack,
      },
      true,
    )
  }

  trackErrors(options?: ErrorOptions): ErrorActions {
    if (this.errorListenerExists || !this.canTrack()) {
      return defaultActions
    }

    this.errorsOptions = options

    window.addEventListener('error', this.captureError)
    this.errorListenerExists = true

    return {
      stop: () => {
        window.removeEventListener('error', this.captureError)
        this.errorListenerExists = false
      },
    }
  }

  submitError(payload: IErrorEventPayload, evokeCallback?: boolean): void {
    const privateData = {
      pid: this.projectID,
    }

    const errorPayload = {
      pg:
        this.activePage ||
        getPath({
          hash: this.pageViewsOptions?.hash,
          search: this.pageViewsOptions?.search,
        }),
      lc: getLocale(),
      tz: getTimezone(),
      ...payload,
    }

    if (evokeCallback && this.errorsOptions?.callback) {
      const callbackResult = this.errorsOptions.callback(errorPayload)

      if (callbackResult === false) {
        return
      }

      if (callbackResult && typeof callbackResult === 'object') {
        Object.assign(errorPayload, callbackResult)
      }
    }

    Object.assign(errorPayload, privateData)

    this.sendRequest('error', errorPayload)
  }

  async track(event: TrackEventOptions): Promise<void> {
    if (!this.canTrack()) {
      return
    }

    const data = {
      ...event,
      pid: this.projectID,
      pg:
        this.activePage ||
        getPath({
          hash: this.pageViewsOptions?.hash,
          search: this.pageViewsOptions?.search,
        }),
      lc: getLocale(),
      tz: getTimezone(),
      ref: getReferrer(),
      so: getUTMSource(),
      me: getUTMMedium(),
      ca: getUTMCampaign(),
      te: getUTMTerm(),
      co: getUTMContent(),
      profileId: event.profileId ?? this.options?.profileId,
    }
    await this.sendRequest('custom', data)
  }

  trackPageViews(options?: PageViewsOptions): PageActions {
    if (!this.canTrack()) {
      return defaultActions
    }

    if (this.pageData) {
      return this.pageData.actions
    }

    this.pageViewsOptions = options
    let interval: NodeJS.Timeout

    if (!options?.unique) {
      interval = setInterval(this.trackPathChange, 2000)
    }

    setTimeout(this.heartbeat, 3000)
    const hbInterval = setInterval(this.heartbeat, 28000)

    const path = getPath({
      hash: options?.hash,
      search: options?.search,
    })

    this.pageData = {
      path,
      actions: {
        stop: () => {
          clearInterval(interval)
          clearInterval(hbInterval)
        },
      },
    }

    this.trackPage(path, options?.unique)
    return this.pageData.actions
  }

  getPerformanceStats(): IPerfPayload | {} {
    if (!this.canTrack() || this.perfStatsCollected || !window.performance?.getEntriesByType) {
      return {}
    }

    const perf = window.performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming

    if (!perf) {
      return {}
    }

    this.perfStatsCollected = true

    return {
      // Network
      dns: perf.domainLookupEnd - perf.domainLookupStart, // DNS Resolution
      tls: perf.secureConnectionStart ? perf.requestStart - perf.secureConnectionStart : 0, // TLS Setup; checking if secureConnectionStart is not 0 (it's 0 for non-https websites)
      conn: perf.secureConnectionStart
        ? perf.secureConnectionStart - perf.connectStart
        : perf.connectEnd - perf.connectStart, // Connection time
      response: perf.responseEnd - perf.responseStart, // Response Time (Download)

      // Frontend
      render: perf.domComplete - perf.domContentLoadedEventEnd, // Browser rendering the HTML time
      dom_load: perf.domContentLoadedEventEnd - perf.responseEnd, // DOM loading timing
      page_load: perf.loadEventStart, // Page load time

      // Backend
      ttfb: perf.responseStart - perf.requestStart,
    }
  }

  /**
   * Fetches all feature flags and experiments for the project.
   * Results are cached for 5 minutes by default.
   *
   * @param options - Options for evaluating feature flags.
   * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
   * @returns A promise that resolves to a record of flag keys to boolean values.
   */
  async getFeatureFlags(options?: FeatureFlagsOptions, forceRefresh?: boolean): Promise<Record<string, boolean>> {
    if (!isInBrowser()) {
      return {}
    }

    const requestedProfileId = options?.profileId ?? this.options?.profileId

    // Check cache first - must match profileId and not be expired
    if (!forceRefresh && this.cachedData) {
      const now = Date.now()
      const isSameProfile = this.cachedData.profileId === requestedProfileId
      if (isSameProfile && now - this.cachedData.timestamp < DEFAULT_CACHE_DURATION) {
        return this.cachedData.flags
      }
    }

    try {
      await this.fetchFlagsAndExperiments(options)
      return this.cachedData?.flags || {}
    } catch (error) {
      console.warn('[Swetrix] Error fetching feature flags:', error)
      return this.cachedData?.flags || {}
    }
  }

  /**
   * Internal method to fetch both feature flags and experiments from the API.
   */
  private async fetchFlagsAndExperiments(options?: FeatureFlagsOptions | ExperimentOptions): Promise<void> {
    const apiBase = this.getApiBase()
    const body: { pid: string; profileId?: string } = {
      pid: this.projectID,
    }

    // Use profileId from options, or fall back to global profileId
    const profileId = options?.profileId ?? this.options?.profileId
    if (profileId) {
      body.profileId = profileId
    }

    const response = await fetch(`${apiBase}/feature-flag/evaluate`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    })

    if (!response.ok) {
      console.warn('[Swetrix] Failed to fetch feature flags and experiments:', response.status)
      return
    }

    const data = (await response.json()) as {
      flags: Record<string, boolean>
      experiments?: Record<string, string>
    }

    // Use profileId from options, or fall back to global profileId
    const cachedProfileId = options?.profileId ?? this.options?.profileId

    // Update cache with both flags and experiments
    this.cachedData = {
      flags: data.flags || {},
      experiments: data.experiments || {},
      timestamp: Date.now(),
      profileId: cachedProfileId,
    }
  }

  /**
   * Gets the value of a single feature flag.
   *
   * @param key - The feature flag key.
   * @param options - Options for evaluating the feature flag.
   * @param defaultValue - Default value to return if the flag is not found. Defaults to false.
   * @returns A promise that resolves to the boolean value of the flag.
   */
  async getFeatureFlag(key: string, options?: FeatureFlagsOptions, defaultValue: boolean = false): Promise<boolean> {
    const flags = await this.getFeatureFlags(options)
    return flags[key] ?? defaultValue
  }

  /**
   * Clears the cached feature flags and experiments, forcing a fresh fetch on the next call.
   */
  clearFeatureFlagsCache(): void {
    this.cachedData = null
  }

  /**
   * Fetches all A/B test experiments for the project.
   * Results are cached for 5 minutes by default (shared cache with feature flags).
   *
   * @param options - Options for evaluating experiments.
   * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
   * @returns A promise that resolves to a record of experiment IDs to variant keys.
   *
   * @example
   * ```typescript
   * const experiments = await getExperiments()
   * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }
   * ```
   */
  async getExperiments(options?: ExperimentOptions, forceRefresh?: boolean): Promise<Record<string, string>> {
    if (!isInBrowser()) {
      return {}
    }

    const requestedProfileId = options?.profileId ?? this.options?.profileId

    // Check cache first - must match profileId and not be expired
    if (!forceRefresh && this.cachedData) {
      const now = Date.now()
      const isSameProfile = this.cachedData.profileId === requestedProfileId
      if (isSameProfile && now - this.cachedData.timestamp < DEFAULT_CACHE_DURATION) {
        return this.cachedData.experiments
      }
    }

    try {
      await this.fetchFlagsAndExperiments(options)
      return this.cachedData?.experiments || {}
    } catch (error) {
      console.warn('[Swetrix] Error fetching experiments:', error)
      return this.cachedData?.experiments || {}
    }
  }

  /**
   * Gets the variant key for a specific A/B test experiment.
   *
   * @param experimentId - The experiment ID.
   * @param options - Options for evaluating the experiment.
   * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.
   * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.
   *
   * @example
   * ```typescript
   * const variant = await getExperiment('checkout-redesign')
   *
   * if (variant === 'new-checkout') {
   *   // Show new checkout flow
   * } else {
   *   // Show control (original) checkout
   * }
   * ```
   */
  async getExperiment(
    experimentId: string,
    options?: ExperimentOptions,
    defaultVariant: string | null = null,
  ): Promise<string | null> {
    const experiments = await this.getExperiments(options)
    return experiments[experimentId] ?? defaultVariant
  }

  /**
   * Clears the cached experiments (alias for clearFeatureFlagsCache since they share the same cache).
   */
  clearExperimentsCache(): void {
    this.cachedData = null
  }

  /**
   * Gets the anonymous profile ID for the current visitor.
   * If profileId was set via init options, returns that.
   * Otherwise, requests server to generate one from IP/UA hash.
   *
   * This ID can be used for revenue attribution with payment providers.
   *
   * @returns A promise that resolves to the profile ID string, or null on error.
   *
   * @example
   * ```typescript
   * const profileId = await swetrix.getProfileId()
   *
   * // Pass to Paddle Checkout for revenue attribution
   * Paddle.Checkout.open({
   *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
   *   customData: {
   *     swetrix_profile_id: profileId,
   *     swetrix_session_id: await swetrix.getSessionId()
   *   }
   * })
   * ```
   */
  async getProfileId(): Promise<string | null> {
    // If profileId is already set in options, return it
    if (this.options?.profileId) {
      return this.options.profileId
    }

    if (!isInBrowser()) {
      return null
    }

    try {
      const apiBase = this.getApiBase()
      const response = await fetch(`${apiBase}/log/profile-id`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ pid: this.projectID }),
      })

      if (!response.ok) {
        return null
      }

      const data = (await response.json()) as { profileId: string | null }
      return data.profileId
    } catch {
      return null
    }
  }

  /**
   * Gets the current session ID for the visitor.
   * Session IDs are generated server-side based on IP and user agent.
   *
   * This ID can be used for revenue attribution with payment providers.
   *
   * @returns A promise that resolves to the session ID string, or null on error.
   *
   * @example
   * ```typescript
   * const sessionId = await swetrix.getSessionId()
   *
   * // Pass to Paddle Checkout for revenue attribution
   * Paddle.Checkout.open({
   *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
   *   customData: {
   *     swetrix_profile_id: await swetrix.getProfileId(),
   *     swetrix_session_id: sessionId
   *   }
   * })
   * ```
   */
  async getSessionId(): Promise<string | null> {
    if (!isInBrowser()) {
      return null
    }

    try {
      const apiBase = this.getApiBase()
      const response = await fetch(`${apiBase}/log/session-id`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ pid: this.projectID }),
      })

      if (!response.ok) {
        return null
      }

      const data = (await response.json()) as { sessionId: string | null }
      return data.sessionId
    } catch {
      return null
    }
  }

  /**
   * Gets the API base URL (without /log suffix).
   */
  private getApiBase(): string {
    if (this.options?.apiURL) {
      // Remove trailing /log if present
      return this.options.apiURL.replace(/\/log\/?$/, '')
    }
    return DEFAULT_API_BASE
  }

  private heartbeat(): void {
    if (!this.pageViewsOptions?.heartbeatOnBackground && document.visibilityState === 'hidden') {
      return
    }

    const data: { pid: string; profileId?: string } = {
      pid: this.projectID,
    }

    if (this.options?.profileId) {
      data.profileId = this.options.profileId
    }

    this.sendRequest('hb', data)
  }

  // Tracking path changes. If path changes -> calling this.trackPage method
  private trackPathChange(): void {
    if (!this.pageData) return
    const newPath = getPath({
      hash: this.pageViewsOptions?.hash,
      search: this.pageViewsOptions?.search,
    })
    const { path } = this.pageData

    if (path !== newPath) {
      this.trackPage(newPath, false)
    }
  }

  private trackPage(pg: string, unique: boolean = false): void {
    if (!this.pageData) return
    this.pageData.path = pg

    const perf = this.getPerformanceStats()

    this.activePage = pg
    this.submitPageView({ pg }, unique, perf, true)
  }

  submitPageView(
    payload: Partial<IPageViewPayload>,
    unique: boolean,
    perf: IPerfPayload | {},
    evokeCallback?: boolean,
  ): void {
    const privateData = {
      pid: this.projectID,
      perf,
      unique,
    }

    const pvPayload = {
      lc: getLocale(),
      tz: getTimezone(),
      ref: getReferrer(),
      so: getUTMSource(),
      me: getUTMMedium(),
      ca: getUTMCampaign(),
      te: getUTMTerm(),
      co: getUTMContent(),
      profileId: this.options?.profileId,
      ...payload,
    }

    if (evokeCallback && this.pageViewsOptions?.callback) {
      const callbackResult = this.pageViewsOptions.callback(pvPayload as IPageViewPayload)

      if (callbackResult === false) {
        return
      }

      if (callbackResult && typeof callbackResult === 'object') {
        Object.assign(pvPayload, callbackResult)
      }
    }

    Object.assign(pvPayload, privateData)

    this.sendRequest('', pvPayload)
  }

  private canTrack(): boolean {
    if (
      this.options?.disabled ||
      !isInBrowser() ||
      (this.options?.respectDNT && window.navigator?.doNotTrack === '1') ||
      (!this.options?.devMode && isLocalhost()) ||
      isAutomated()
    ) {
      return false
    }

    return true
  }

  private async sendRequest(path: string, body: object): Promise<void> {
    const host = this.options?.apiURL || DEFAULT_API_HOST
    await fetch(`${host}/${path}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    })
  }
}


================================================
FILE: src/index.ts
================================================
import {
  Lib,
  LibOptions,
  TrackEventOptions,
  PageViewsOptions,
  ErrorOptions,
  PageActions,
  ErrorActions,
  defaultActions,
  IErrorEventPayload,
  IPageViewPayload,
  FeatureFlagsOptions,
  ExperimentOptions,
} from './Lib.js'

export let LIB_INSTANCE: Lib | null = null

/**
 * Initialise the tracking library instance (other methods won't work if the library is not initialised).
 *
 * @param {string} pid The Project ID to link the instance of Swetrix.js to.
 * @param {LibOptions} options Options related to the tracking.
 * @returns {Lib} Instance of the Swetrix.js.
 */
export function init(pid: string, options?: LibOptions): Lib {
  if (!LIB_INSTANCE) {
    LIB_INSTANCE = new Lib(pid, options)
  }

  return LIB_INSTANCE
}

/**
 * With this function you are able to track any custom events you want.
 * You should never send any identifiable data (like User ID, email, session cookie, etc.) as an event name.
 * The total number of track calls and their conversion rate will be saved.
 *
 * @param {TrackEventOptions} event The options related to the custom event.
 */
export async function track(event: TrackEventOptions): Promise<void> {
  if (!LIB_INSTANCE) return

  await LIB_INSTANCE.track(event)
}

/**
 * With this function you are able to automatically track pageviews across your application.
 *
 * @param {PageViewsOptions} options Pageviews tracking options.
 * @returns {PageActions} The actions related to the tracking. Used to stop tracking pages.
 */
export function trackViews(options?: PageViewsOptions): Promise<PageActions> {
  return new Promise((resolve) => {
    if (!LIB_INSTANCE) {
      resolve(defaultActions)
      return
    }

    // We need to verify that document.readyState is complete for the performance stats to be collected correctly.
    if (typeof document === 'undefined' || document.readyState === 'complete') {
      resolve(LIB_INSTANCE.trackPageViews(options))
    } else {
      window.addEventListener('load', () => {
        // @ts-ignore
        resolve(LIB_INSTANCE.trackPageViews(options))
      })
    }
  })
}

/**
 * This function is used to set up automatic error events tracking.
 * It set's up an error listener, and whenever an error happens, it gets tracked.
 *
 * @returns {ErrorActions} The actions related to the tracking. Used to stop tracking errors.
 */
export function trackErrors(options?: ErrorOptions): ErrorActions {
  if (!LIB_INSTANCE) {
    return defaultActions
  }

  return LIB_INSTANCE.trackErrors(options)
}

/**
 * This function is used to manually track an error event.
 * It's useful if you want to track specific errors in your application.
 *
 * @param payload Swetrix error object to send.
 * @returns void
 */
export function trackError(payload: IErrorEventPayload): void {
  if (!LIB_INSTANCE) return

  LIB_INSTANCE.submitError(payload, false)
}

/**
 * This function is used to manually track a page view event.
 * It's useful if your application uses esoteric routing which is not supported by Swetrix by default.
 *
 * @deprecated This function is deprecated and will be removed soon, please use the `pageview` instead.
 * @param pg Path of the page to track (this will be sent to the Swetrix API and displayed in the dashboard).
 * @param _prev Path of the previous page (deprecated and ignored).
 * @param unique If set to `true`, only 1 event with the same ID will be saved per user session.
 * @returns void
 */
export function trackPageview(pg: string, _prev?: string, unique?: boolean): void {
  if (!LIB_INSTANCE) return

  LIB_INSTANCE.submitPageView({ pg }, Boolean(unique), {})
}

export interface IPageviewOptions {
  payload: Partial<IPageViewPayload>
  unique?: boolean
}

export function pageview(options: IPageviewOptions): void {
  if (!LIB_INSTANCE) return

  LIB_INSTANCE.submitPageView(options.payload, Boolean(options.unique), {})
}

/**
 * Fetches all feature flags for the project.
 * Results are cached for 5 minutes by default.
 *
 * @param options - Options for evaluating feature flags (visitorId, attributes).
 * @param forceRefresh - If true, bypasses the cache and fetches fresh flags.
 * @returns A promise that resolves to a record of flag keys to boolean values.
 *
 * @example
 * ```typescript
 * const flags = await getFeatureFlags({
 *   visitorId: 'user-123',
 *   attributes: { cc: 'US', dv: 'desktop' }
 * })
 *
 * if (flags['new-checkout']) {
 *   // Show new checkout flow
 * }
 * ```
 */
export async function getFeatureFlags(
  options?: FeatureFlagsOptions,
  forceRefresh?: boolean,
): Promise<Record<string, boolean>> {
  if (!LIB_INSTANCE) return {}

  return LIB_INSTANCE.getFeatureFlags(options, forceRefresh)
}

/**
 * Gets the value of a single feature flag.
 *
 * @param key - The feature flag key.
 * @param options - Options for evaluating the feature flag (visitorId, attributes).
 * @param defaultValue - Default value to return if the flag is not found. Defaults to false.
 * @returns A promise that resolves to the boolean value of the flag.
 *
 * @example
 * ```typescript
 * const isEnabled = await getFeatureFlag('dark-mode', { visitorId: 'user-123' })
 *
 * if (isEnabled) {
 *   // Enable dark mode
 * }
 * ```
 */
export async function getFeatureFlag(
  key: string,
  options?: FeatureFlagsOptions,
  defaultValue: boolean = false,
): Promise<boolean> {
  if (!LIB_INSTANCE) return defaultValue

  return LIB_INSTANCE.getFeatureFlag(key, options, defaultValue)
}

/**
 * Clears the cached feature flags, forcing a fresh fetch on the next call.
 * Useful when you know the user's context has changed significantly.
 */
export function clearFeatureFlagsCache(): void {
  if (!LIB_INSTANCE) return

  LIB_INSTANCE.clearFeatureFlagsCache()
}

/**
 * Fetches all A/B test experiments for the project.
 * Results are cached for 5 minutes by default (shared cache with feature flags).
 *
 * @param options - Options for evaluating experiments.
 * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
 * @returns A promise that resolves to a record of experiment IDs to variant keys.
 *
 * @example
 * ```typescript
 * const experiments = await getExperiments()
 * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }
 *
 * // Use the assigned variant
 * const checkoutVariant = experiments['checkout-experiment-id']
 * if (checkoutVariant === 'new-checkout') {
 *   showNewCheckout()
 * } else {
 *   showOriginalCheckout()
 * }
 * ```
 */
export async function getExperiments(
  options?: ExperimentOptions,
  forceRefresh?: boolean,
): Promise<Record<string, string>> {
  if (!LIB_INSTANCE) return {}

  return LIB_INSTANCE.getExperiments(options, forceRefresh)
}

/**
 * Gets the variant key for a specific A/B test experiment.
 *
 * @param experimentId - The experiment ID.
 * @param options - Options for evaluating the experiment.
 * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.
 * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.
 *
 * @example
 * ```typescript
 * const variant = await getExperiment('checkout-redesign-experiment-id')
 *
 * if (variant === 'new-checkout') {
 *   // Show new checkout flow
 *   showNewCheckout()
 * } else if (variant === 'control') {
 *   // Show original checkout (control group)
 *   showOriginalCheckout()
 * } else {
 *   // Experiment not running or user not included
 *   showOriginalCheckout()
 * }
 * ```
 */
export async function getExperiment(
  experimentId: string,
  options?: ExperimentOptions,
  defaultVariant: string | null = null,
): Promise<string | null> {
  if (!LIB_INSTANCE) return defaultVariant

  return LIB_INSTANCE.getExperiment(experimentId, options, defaultVariant)
}

/**
 * Clears the cached experiments, forcing a fresh fetch on the next call.
 * This is an alias for clearFeatureFlagsCache since experiments and flags share the same cache.
 */
export function clearExperimentsCache(): void {
  if (!LIB_INSTANCE) return

  LIB_INSTANCE.clearExperimentsCache()
}

/**
 * Gets the anonymous profile ID for the current visitor.
 * If profileId was set via init options, returns that.
 * Otherwise, requests server to generate one from IP/UA hash.
 *
 * This ID can be used for revenue attribution with payment providers like Paddle.
 *
 * @returns A promise that resolves to the profile ID string, or null on error.
 *
 * @example
 * ```typescript
 * const profileId = await getProfileId()
 *
 * // Pass to Paddle Checkout for revenue attribution
 * Paddle.Checkout.open({
 *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
 *   customData: {
 *     swetrix_profile_id: profileId,
 *     swetrix_session_id: await getSessionId()
 *   }
 * })
 * ```
 */
export async function getProfileId(): Promise<string | null> {
  if (!LIB_INSTANCE) return null

  return LIB_INSTANCE.getProfileId()
}

/**
 * Gets the current session ID for the visitor.
 * Session IDs are generated server-side based on IP and user agent.
 *
 * This ID can be used for revenue attribution with payment providers like Paddle.
 *
 * @returns A promise that resolves to the session ID string, or null on error.
 *
 * @example
 * ```typescript
 * const sessionId = await getSessionId()
 *
 * // Pass to Paddle Checkout for revenue attribution
 * Paddle.Checkout.open({
 *   items: [{ priceId: 'pri_01234567890', quantity: 1 }],
 *   customData: {
 *     swetrix_profile_id: await getProfileId(),
 *     swetrix_session_id: sessionId
 *   }
 * })
 * ```
 */
export async function getSessionId(): Promise<string | null> {
  if (!LIB_INSTANCE) return null

  return LIB_INSTANCE.getSessionId()
}

export {
  LibOptions,
  TrackEventOptions,
  PageViewsOptions,
  ErrorOptions,
  PageActions,
  ErrorActions,
  IErrorEventPayload,
  IPageViewPayload,
  FeatureFlagsOptions,
  ExperimentOptions,
}


================================================
FILE: src/utils.ts
================================================
interface IGetPath {
  hash?: boolean
  search?: boolean
}

const findInSearch = (exp: RegExp): string | undefined => {
  const res = location.search.match(exp)
  return (res && res[2]) || undefined
}

const utmSourceRegex = /[?&](ref|source|utm_source|gad_source)=([^?&]+)/
const utmCampaignRegex = /[?&](utm_campaign|gad_campaignid)=([^?&]+)/
const utmMediumRegex = /[?&](utm_medium)=([^?&]+)/
const utmTermRegex = /[?&](utm_term)=([^?&]+)/
const utmContentRegex = /[?&](utm_content)=([^?&]+)/

const gclidRegex = /[?&](gclid)=([^?&]+)/

const getGclid = () => {
  return findInSearch(gclidRegex) ? '<gclid>' : undefined
}

export const isInBrowser = () => {
  return typeof window !== 'undefined'
}

export const isLocalhost = () => {
  return location?.hostname === 'localhost' || location?.hostname === '127.0.0.1' || location?.hostname === ''
}

export const isAutomated = () => {
  return navigator?.webdriver
}

export const getLocale = () => {
  return typeof navigator.languages !== 'undefined' ? navigator.languages[0] : navigator.language
}

export const getTimezone = () => {
  try {
    return Intl.DateTimeFormat().resolvedOptions().timeZone
  } catch (e) {
    return
  }
}

export const getReferrer = (): string | undefined => {
  return document.referrer || undefined
}

export const getUTMSource = () => findInSearch(utmSourceRegex)

export const getUTMMedium = () => findInSearch(utmMediumRegex) || getGclid()

export const getUTMCampaign = () => findInSearch(utmCampaignRegex)

export const getUTMTerm = () => findInSearch(utmTermRegex)

export const getUTMContent = () => findInSearch(utmContentRegex)

/**
 * Function used to track the current page (path) of the application.
 * Will work in cases where the path looks like:
 * - /path
 * - /#/path
 * - /path?search
 * - /path?search#hash
 * - /path#hash?search
 *
 * @param options - Options for the function.
 * @param options.hash - Whether to trigger on hash change.
 * @param options.search - Whether to trigger on search change.
 * @returns The path of the current page.
 */
export const getPath = (options: IGetPath): string => {
  let result = location.pathname || ''

  if (options.hash) {
    const hashIndex = location.hash.indexOf('?')
    const hashString = hashIndex > -1 ? location.hash.substring(0, hashIndex) : location.hash
    result += hashString
  }

  if (options.search) {
    const hashIndex = location.hash.indexOf('?')
    const searchString = location.search || (hashIndex > -1 ? location.hash.substring(hashIndex) : '')
    result += searchString
  }

  return result
}


================================================
FILE: tests/README.md
================================================
# Swetrix JS Tests

This directory contains tests for the Swetrix JavaScript analytics client.

## Test Structure

- **initialisation.test.ts**: Tests for library initialisation and core functionality
- **pageview.test.ts**: Tests for page view tracking functionality
- **events.test.ts**: Tests for custom event tracking
- **errors.test.ts**: Tests for error tracking
- **utils.test.ts**: Tests for utility functions (utils.ts file)

## Running Tests

To run the tests, use:

```bash
npm test
```

For watching mode:

```bash
npm run test:watch
```

## Test Environment

Tests use Jest with jsdom environment to simulate a browser environment. The library is tested in isolation with mocked API requests.

## Mocking Strategy

- API requests are mocked to avoid actual network requests
- Browser APIs (window, document, navigator) are mocked as needed
- The library's internal methods are selectively mocked or spied on to verify behaviour

## Writing New Tests

When adding new tests:

1. Follow the existing patterns of mocking and setup
2. Use descriptive test names that explain what aspect is being tested
3. Structure tests with Arrange-Act-Assert pattern
4. Clean up mocks between tests using beforeEach/afterEach

## Coverage

Test coverage can be checked by running:

```bash
npm test -- --coverage
```


================================================
FILE: tests/errors.test.ts
================================================
import { init, trackError, trackErrors } from '../src/index'
import { Lib } from '../src/Lib'

jest.mock('../src/Lib', () => {
  const originalModule = jest.requireActual('../src/Lib')

  return {
    ...originalModule,
    Lib: class MockLib extends originalModule.Lib {
      sendRequest = jest.fn().mockResolvedValue(undefined)

      captureError = jest.fn().mockImplementation(function (this: any, event: ErrorEvent) {
        const errorPayload = {
          name: event.error?.name || 'Error',
          message: event.message,
          filename: event.filename,
          lineno: event.lineno,
          colno: event.colno,
          stackTrace: event.error?.stack,
        }

        this.submitError(errorPayload, true)
      })

      submitError = jest.fn().mockImplementation(function (this: any, payload: any, evokeCallback: boolean = true) {
        const formattedPayload = {
          pid: this.projectID,
          name: payload.name,
          message: payload.message,
          filename: payload.filename,
          lineno: payload.lineno,
          colno: payload.colno,
          stackTrace: payload.stackTrace,
          meta: payload.meta,
          pg: '/test-page',
          lc: 'en-US',
          tz: 'Europe/London',
        }

        // Simulate callback behavior if evokeCallback is true and callback exists
        if (evokeCallback && this.errorsOptions?.callback) {
          const callbackResult = this.errorsOptions.callback(formattedPayload)

          if (callbackResult === false) {
            return
          }

          if (callbackResult && typeof callbackResult === 'object') {
            Object.assign(formattedPayload, callbackResult)
          }
        }

        this.sendRequest('error', formattedPayload)
      })

      trackErrors(options?: any) {
        return {
          stop: () => {},
        }
      }
    },
  }
})

describe('Error Tracking', () => {
  const PROJECT_ID = 'test-project-id'
  let libInstance: Lib

  beforeEach(() => {
    jest.clearAllMocks()

    libInstance = init(PROJECT_ID, { devMode: true }) as Lib

    Object.defineProperty(window, 'location', {
      value: {
        hostname: 'example.com',
        pathname: '/test-page',
        hash: '',
        search: '',
      },
      writable: true,
    })

    window.addEventListener = jest.fn()
    window.removeEventListener = jest.fn()
  })

  test('trackError function should track an error event', () => {
    // Arrange
    const errorPayload = {
      name: 'TypeError',
      message: 'Cannot read property of undefined',
      filename: 'app.js',
      lineno: 42,
      colno: 10,
    }

    // Act
    trackError(errorPayload)

    // Assert
    expect((libInstance as any).sendRequest).toHaveBeenCalledTimes(1)
    // We've changed our expectation to match the actual format used by the library
    expect((libInstance as any).sendRequest).toHaveBeenCalledWith(
      'error',
      expect.objectContaining({
        pid: PROJECT_ID,
        name: errorPayload.name,
        message: errorPayload.message,
        filename: errorPayload.filename,
        lineno: errorPayload.lineno,
        colno: errorPayload.colno,
      }),
    )
  })

  test('trackErrors function should return actions object', () => {
    // Mock the trackErrors method
    const trackErrorsSpy = jest.spyOn(libInstance, 'trackErrors')

    // Act
    const actions = trackErrors()

    // Assert
    expect(trackErrorsSpy).toHaveBeenCalled()
    expect(actions).toHaveProperty('stop')
    expect(typeof actions.stop).toBe('function')
  })

  test('trackErrors with sample rate should pass the sampleRate option', () => {
    // Mock the trackErrors method
    const trackErrorsSpy = jest.spyOn(libInstance, 'trackErrors')

    // Act
    trackErrors({ sampleRate: 0.5 })

    // Assert
    expect(trackErrorsSpy).toHaveBeenCalledWith({ sampleRate: 0.5 })
  })

  test('trackErrors with callback should pass the callback option', () => {
    // Set up a callback
    const callbackFn = jest.fn().mockReturnValue({
      name: 'CustomError',
      message: 'Modified by callback',
    })

    // Mock the trackErrors method
    const trackErrorsSpy = jest.spyOn(libInstance, 'trackErrors')

    // Act
    trackErrors({ callback: callbackFn })

    // Assert
    expect(trackErrorsSpy).toHaveBeenCalledWith({ callback: callbackFn })
  })

  test('trackErrors stop function should call the returned stop function', () => {
    // Arrange
    const mockStop = jest.fn()
    jest.spyOn(libInstance, 'trackErrors').mockReturnValue({ stop: mockStop })

    // Act
    const actions = trackErrors()
    actions.stop()

    // Assert
    expect(mockStop).toHaveBeenCalled()
  })

  test('trackError should handle meta property', () => {
    // Arrange
    const errorPayload = {
      name: 'ValidationError',
      message: 'Invalid input provided',
      filename: 'validation.js',
      lineno: 15,
      colno: 5,
      meta: {
        userId: 'user123',
        feature: 'login',
        environment: 'production',
      },
    }

    // Act
    trackError(errorPayload)

    // Assert
    expect((libInstance as any).sendRequest).toHaveBeenCalledWith(
      'error',
      expect.objectContaining({
        pid: PROJECT_ID,
        name: errorPayload.name,
        message: errorPayload.message,
        filename: errorPayload.filename,
        lineno: errorPayload.lineno,
        colno: errorPayload.colno,
        meta: errorPayload.meta,
      }),
    )
  })

  test('captureError should automatically extract stackTrace from ErrorEvent', () => {
    // Arrange
    const mockError = new Error('Test error message')
    mockError.stack = 'Error: Test error message\n    at test.js:10:5\n    at Object.run (test.js:5:2)'

    const errorEvent = new ErrorEvent('error', {
      error: mockError,
      message: 'Test error message',
      filename: 'test.js',
      lineno: 10,
      colno: 5,
    })

    // Act
    ;(libInstance as any).captureError(errorEvent)

    // Assert
    expect((libInstance as any).submitError).toHaveBeenCalledWith(
      expect.objectContaining({
        name: 'Error',
        message: 'Test error message',
        filename: 'test.js',
        lineno: 10,
        colno: 5,
        stackTrace: mockError.stack,
      }),
      true,
    )
  })
})


================================================
FILE: tests/events.test.ts
================================================
import { init, track } from '../src/index'
import { Lib } from '../src/Lib'

jest.mock('../src/Lib', () => {
  const originalModule = jest.requireActual('../src/Lib')

  return {
    ...originalModule,
    Lib: class MockLib extends originalModule.Lib {
      sendRequest = jest.fn().mockResolvedValue(undefined)
    },
  }
})

describe('Custom Event Tracking', () => {
  const PROJECT_ID = 'test-project-id'
  let libInstance: Lib

  beforeEach(() => {
    jest.clearAllMocks()

    libInstance = init(PROJECT_ID, { devMode: true }) as Lib

    Object.defineProperty(window, 'location', {
      value: {
        hostname: 'example.com',
        pathname: '/test-page',
        hash: '',
        search: '',
      },
      writable: true,
    })
  })

  test('track function should track a custom event', async () => {
    // Arrange
    const eventName = 'button_click'

    // Act
    await track({ ev: eventName })

    // Assert
    expect((libInstance as any).sendRequest).toHaveBeenCalledTimes(1)
    expect((libInstance as any).sendRequest).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({
        pid: PROJECT_ID,
        ev: eventName,
      }),
    )
  })

  test('track function with unique flag should track unique event', async () => {
    // Arrange
    const eventName = 'form_submit'

    // Act
    await track({ ev: eventName, unique: true })

    // Assert
    expect((libInstance as any).sendRequest).toHaveBeenCalledTimes(1)
    expect((libInstance as any).sendRequest).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({
        pid: PROJECT_ID,
        ev: eventName,
        unique: true,
      }),
    )
  })

  test('track function with metadata should include metadata in request', async () => {
    // Arrange
    const eventName = 'purchase'
    const metadata = {
      product: 'premium_plan',
      price: '99.99',
    }

    // Act
    await track({
      ev: eventName,
      meta: metadata,
    })

    // Assert
    expect((libInstance as any).sendRequest).toHaveBeenCalledTimes(1)
    expect((libInstance as any).sendRequest).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({
        pid: PROJECT_ID,
        ev: eventName,
        meta: metadata,
      }),
    )
  })

  test('should not track when library is not initialized', async () => {
    // Create a new module import to reset the LIB_INSTANCE
    jest.resetModules()
    const { track: newTrack } = await import('../src/index')

    // Act
    await newTrack({ ev: 'test_event' })

    // Assert - no request should be sent
    expect((libInstance as any).sendRequest).not.toHaveBeenCalled()
  })
})


================================================
FILE: tests/experiments.test.ts
================================================
import { init, getExperiment, getExperiments, clearExperimentsCache } from '../src/index'
import { Lib } from '../src/Lib'

// Mock fetch globally
const mockFetch = jest.fn()
global.fetch = mockFetch

describe('A/B Testing Experiments', () => {
  const PROJECT_ID = 'test-project-id'
  let libInstance: Lib

  beforeEach(() => {
    jest.clearAllMocks()
    jest.resetModules()

    // Reset fetch mock
    mockFetch.mockReset()

    Object.defineProperty(window, 'location', {
      value: {
        hostname: 'example.com',
        pathname: '/test-page',
        hash: '',
        search: '',
      },
      writable: true,
    })
  })

  describe('getExperiments', () => {
    beforeEach(async () => {
      jest.resetModules()
      const { init: freshInit } = await import('../src/index')
      libInstance = freshInit(PROJECT_ID, { devMode: true }) as Lib
    })

    test('should return experiments from API response', async () => {
      // Arrange
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          flags: { 'feature-flag-1': true },
          experiments: {
            'exp-checkout': 'variant-b',
            'exp-pricing': 'control',
          },
        }),
      })

      // Act
      const { getExperiments: freshGetExperiments } = await import('../src/index')
      const experiments = await freshGetExperiments()

      // Assert
      expect(experiments).toEqual({
        'exp-checkout': 'variant-b',
        'exp-pricing': 'control',
      })
    })

    test('should return empty object when no experiments in response', async () => {
      // Arrange
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          flags: { 'feature-flag-1': true },
        }),
      })

      // Act
      const { getExperiments: freshGetExperiments } = await import('../src/index')
      const experiments = await freshGetExperiments()

      // Assert
      expect(experiments).toEqual({})
    })

    test('should use cached experiments on subsequent calls', async () => {
      // Arrange
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          flags: {},
          experiments: { 'exp-1': 'variant-a' },
        }),
      })

      // Act
      const { getExperiments: freshGetExperiments } = await import('../src/index')
      await freshGetExperiments()
      await freshGetExperiments()

      // Assert - fetch should only be called once
      expect(mockFetch).toHaveBeenCalledTimes(1)
    })

    test('should bypass cache when forceRefresh is true', async () => {
      // Arrange
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({
            flags: {},
            experiments: { 'exp-1': 'variant-a' },
          }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({
            flags: {},
            experiments: { 'exp-1': 'variant-b' },
          }),
        })

      // Act
      const { getExperiments: freshGetExperiments } = await import('../src/index')
      const first = await freshGetExperiments()
      const second = await freshGetExperiments(undefined, true)

      // Assert
      expect(mockFetch).toHaveBeenCalledTimes(2)
      expect(first).toEqual({ 'exp-1': 'variant-a' })
      expect(second).toEqual({ 'exp-1': 'variant-b' })
    })
  })

  describe('getExperiment', () => {
    beforeEach(async () => {
      jest.resetModules()
      const { init: freshInit } = await import('../src/index')
      libInstance = freshInit(PROJECT_ID, { devMode: true }) as Lib
    })

    test('should return specific experiment variant', async () => {
      // Arrange
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          flags: {},
          experiments: {
            'exp-checkout': 'new-checkout',
            'exp-pricing': 'control',
          },
        }),
      })

      // Act
      const { getExperiment: freshGetExperiment } = await import('../src/index')
      const variant = await freshGetExperiment('exp-checkout')

      // Assert
      expect(variant).toBe('new-checkout')
    })

    test('should return defaultVariant when experiment not found', async () => {
      // Arrange
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          flags: {},
          experiments: {},
        }),
      })

      // Act
      const { getExperiment: freshGetExperiment } = await import('../src/index')
      const variant = await freshGetExperiment('non-existent', undefined, 'fallback')

      // Assert
      expect(variant).toBe('fallback')
    })

    test('should return null when experiment not found and no default provided', async () => {
      // Arrange
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          flags: {},
          experiments: {},
        }),
      })

      // Act
      const { getExperiment: freshGetExperiment } = await import('../src/index')
      const variant = await freshGetExperiment('non-existent')

      // Assert
      expect(variant).toBeNull()
    })
  })

  describe('clearExperimentsCache', () => {
    test('should clear cache and fetch fresh data on next call', async () => {
      jest.resetModules()
      const {
        init: freshInit,
        getExperiments: freshGetExperiments,
        clearExperimentsCache: freshClearCache,
      } = await import('../src/index')
      freshInit(PROJECT_ID, { devMode: true })

      // Arrange
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({
            flags: {},
            experiments: { 'exp-1': 'variant-a' },
          }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: async () => ({
            flags: {},
            experiments: { 'exp-1': 'variant-b' },
          }),
        })

      // Act
      await freshGetExperiments()
      freshClearCache()
      const newExperiments = await freshGetExperiments()

      // Assert
      expect(mockFetch).toHaveBeenCalledTimes(2)
      expect(newExperiments).toEqual({ 'exp-1': 'variant-b' })
    })
  })

  describe('profileId handling', () => {
    test('should include profileId in request when provided in options', async () => {
      jest.resetModules()
      const { init: freshInit, getExperiments: freshGetExperiments } = await import('../src/index')
      freshInit(PROJECT_ID, { devMode: true })

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          flags: {},
          experiments: {},
        }),
      })

      // Act
      await freshGetExperiments({ profileId: 'user-123' })

      // Assert
      expect(mockFetch).toHaveBeenCalledWith(
        expect.any(String),
        expect.objectContaining({
          body: JSON.stringify({
            pid: PROJECT_ID,
            profileId: 'user-123',
          }),
        }),
      )
    })

    test('should use global profileId when not provided in options', async () => {
      jest.resetModules()
      const { init: freshInit, getExperiments: freshGetExperiments } = await import('../src/index')
      freshInit(PROJECT_ID, { devMode: true, profileId: 'global-user' })

      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({
          flags: {},
          experiments: {},
        }),
      })

      // Act
      await freshGetExperiments()

      // Assert
      expect(mockFetch).toHaveBeenCalledWith(
        expect.any(String),
        expect.objectContaining({
          body: JSON.stringify({
            pid: PROJECT_ID,
            profileId: 'global-user',
          }),
        }),
      )
    })
  })

  describe('error handling', () => {
    test('should return empty object when fetch fails', async () => {
      jest.resetModules()
      const { init: freshInit, getExperiments: freshGetExperiments } = await import('../src/index')
      freshInit(PROJECT_ID, { devMode: true })

      // Arrange
      mockFetch.mockRejectedValueOnce(new Error('Network error'))

      // Suppress console.warn for this test
      const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()

      // Act
      const experiments = await freshGetExperiments()

      // Assert
      expect(experiments).toEqual({})
      consoleSpy.mockRestore()
    })

    test('should return empty object when response is not ok', async () => {
      jest.resetModules()
      const { init: freshInit, getExperiments: freshGetExperiments } = await import('../src/index')
      freshInit(PROJECT_ID, { devMode: true })

      // Arrange
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 500,
      })

      // Suppress console.warn for this test
      const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()

      // Act
      const experiments = await freshGetExperiments()

      // Assert
      expect(experiments).toEqual({})
      consoleSpy.mockRestore()
    })
  })
})


================================================
FILE: tests/initialisation.test.ts
================================================
import { init } from '../src/index'
import { Lib } from '../src/Lib'

jest.mock('../src/Lib', () => {
  const originalModule = jest.requireActual('../src/Lib')

  return {
    ...originalModule,
    Lib: class MockLib extends originalModule.Lib {
      sendRequest = jest.fn().mockResolvedValue(undefined)
      canTrack = jest.fn().mockReturnValue(true)
    },
  }
})

describe('Library Initialisation', () => {
  beforeEach(() => {
    jest.clearAllMocks()

    jest.resetModules()

    Object.defineProperty(window, 'location', {
      value: {
        hostname: 'example.com',
        pathname: '/test-page',
        hash: '',
        search: '',
      },
      writable: true,
    })

    Object.defineProperty(navigator, 'doNotTrack', {
      value: null,
      writable: true,
    })
  })

  test('init should return a Lib instance', () => {
    // Act
    const instance = init('test-project-id')

    // Assert
    expect(instance).toBeInstanceOf(Lib)
  })

  test('init with devMode should work on localhost', () => {
    // Arrange
    Object.defineProperty(window, 'location', {
      value: {
        hostname: 'localhost',
        pathname: '/',
        hash: '',
        search: '',
      },
      writable: true,
    })

    // Act
    const instance = init('test-project-id', { devMode: true })

    // Assert - no error should be thrown
    expect(instance).toBeInstanceOf(Lib)
    expect((instance as any).canTrack()).toBe(true)
  })

  test('init should respect DNT when respectDNT option is true', () => {
    // Arrange
    Object.defineProperty(navigator, 'doNotTrack', {
      value: '1',
      writable: true,
    })

    // Act
    const instance = init('test-project-id', { respectDNT: true })

    // Mock the canTrack method to return false for this instance
    ;(instance as any).canTrack.mockReturnValue(false)

    // Assert
    expect((instance as any).canTrack()).toBe(false)
  })

  test('init should return the same instance when called multiple times', () => {
    // Act
    const instance1 = init('test-project-id')
    const instance2 = init('another-id') // This should be ignored and return the first instance

    // Assert
    expect(instance1).toBe(instance2)
  })
})


================================================
FILE: tests/pageview.test.ts
================================================
import { init, pageview, trackViews } from '../src/index'
import { Lib } from '../src/Lib'

jest.mock('../src/Lib', () => {
  const originalModule = jest.requireActual('../src/Lib')

  return {
    ...originalModule,
    Lib: class MockLib extends originalModule.Lib {
      sendRequest = jest.fn().mockResolvedValue(undefined)
    },
  }
})

describe('Pageview Tracking', () => {
  const PROJECT_ID = 'test-project-id'
  let libInstance: Lib

  beforeEach(() => {
    jest.clearAllMocks()

    libInstance = init(PROJECT_ID, { devMode: true }) as Lib

    Object.defineProperty(window, 'location', {
      value: {
        hostname: 'example.com',
        pathname: '/test-page',
        hash: '',
        search: '',
      },
      writable: true,
    })

    Object.defineProperty(document, 'referrer', {
      value: 'https://google.com',
      writable: true,
    })
  })

  test('pageview function should track a page view', async () => {
    // Arrange
    const path = '/test-page'

    // Act
    pageview({
      payload: { pg: path },
      unique: false,
    })

    // Assert
    expect((libInstance as any).sendRequest).toHaveBeenCalledTimes(1)
    expect((libInstance as any).sendRequest).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({
        pid: PROJECT_ID,
        pg: path,
      }),
    )
  })

  test('pageview function with unique flag should track unique page view', async () => {
    // Arrange
    const path = '/test-page'

    // Act
    pageview({
      payload: { pg: path },
      unique: true,
    })

    // Assert
    expect((libInstance as any).sendRequest).toHaveBeenCalledTimes(1)
    expect((libInstance as any).sendRequest).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({
        pid: PROJECT_ID,
        pg: path,
        unique: true,
      }),
    )
  })

  test('pageview function with metadata should include metadata in request', async () => {
    // Arrange
    const path = '/test-page'
    const metadata = {
      category: 'blog',
      author: 'John Doe',
    }

    // Act
    pageview({
      payload: {
        pg: path,
        meta: metadata,
      },
      unique: false,
    })

    // Assert
    expect((libInstance as any).sendRequest).toHaveBeenCalledTimes(1)
    expect((libInstance as any).sendRequest).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({
        pid: PROJECT_ID,
        pg: path,
        meta: metadata,
      }),
    )
  })

  test('trackViews should start tracking page views', async () => {
    // Mock the trackPageViews method
    ;(libInstance as any).trackPageViews = jest.fn().mockReturnValue({
      stop: jest.fn(),
    })

    // Act
    await trackViews()

    // Assert
    expect((libInstance as any).trackPageViews).toHaveBeenCalledTimes(1)
  })
})


================================================
FILE: tests/utils.test.ts
================================================
import * as utils from '../src/utils'

describe('Utility Functions', () => {
  beforeEach(() => {
    Object.defineProperty(window, 'location', {
      value: {
        hostname: 'example.com',
        pathname: '/test-page',
        hash: '',
        search: '',
      },
      writable: true,
    })

    Object.defineProperty(document, 'referrer', {
      value: 'https://google.com',
      writable: true,
    })

    Object.defineProperty(navigator, 'language', {
      value: 'en-US',
      writable: true,
    })

    Object.defineProperty(navigator, 'languages', {
      value: ['en-US', 'en'],
      writable: true,
    })

    Object.defineProperty(navigator, 'webdriver', {
      value: false,
      writable: true,
    })
  })

  test('isInBrowser should return true in JSDOM environment', () => {
    expect(utils.isInBrowser()).toBe(true)
  })

  test('isLocalhost should detect localhost', () => {
    // Arrange
    Object.defineProperty(window, 'location', {
      value: {
        hostname: 'localhost',
      },
      writable: true,
    })

    // Act & Assert
    expect(utils.isLocalhost()).toB
Download .txt
gitextract_1n1rgte8/

├── .github/
│   ├── funding.yml
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .prettierrc.cjs
├── LICENSE
├── README.md
├── dist/
│   ├── esnext/
│   │   ├── Lib.d.ts
│   │   ├── Lib.js
│   │   ├── index.d.ts
│   │   ├── index.js
│   │   ├── utils.d.ts
│   │   └── utils.js
│   ├── swetrix.cjs.js
│   ├── swetrix.es5.js
│   └── swetrix.js
├── jest.config.js
├── package.json
├── rollup.config.mjs
├── src/
│   ├── Lib.ts
│   ├── index.ts
│   └── utils.ts
├── tests/
│   ├── README.md
│   ├── errors.test.ts
│   ├── events.test.ts
│   ├── experiments.test.ts
│   ├── initialisation.test.ts
│   ├── pageview.test.ts
│   └── utils.test.ts
├── tsconfig.esnext.json
└── tsconfig.json
Download .txt
SYMBOL INDEX (233 symbols across 12 files)

FILE: dist/esnext/Lib.d.ts
  type LibOptions (line 1) | interface LibOptions {
  type TrackEventOptions (line 23) | interface TrackEventOptions {
  type IPageViewPayload (line 35) | interface IPageViewPayload {
  type IErrorEventPayload (line 52) | interface IErrorEventPayload {
  type IInternalErrorEventPayload (line 63) | interface IInternalErrorEventPayload extends IErrorEventPayload {
  type IPerfPayload (line 68) | interface IPerfPayload {
  type FeatureFlagsOptions (line 81) | interface FeatureFlagsOptions {
  type ExperimentOptions (line 92) | interface ExperimentOptions {
  type PageActions (line 103) | interface PageActions {
  type ErrorActions (line 110) | interface ErrorActions {
  type PageData (line 114) | interface PageData {
  type ErrorOptions (line 120) | interface ErrorOptions {
  type PageViewsOptions (line 137) | interface PageViewsOptions {
  class Lib (line 166) | class Lib {

FILE: dist/esnext/Lib.js
  method stop (line 3) | stop() { }
  constant DEFAULT_API_HOST (line 5) | const DEFAULT_API_HOST = 'https://api.swetrix.com/log';
  constant DEFAULT_API_BASE (line 6) | const DEFAULT_API_BASE = 'https://api.swetrix.com';
  constant DEFAULT_CACHE_DURATION (line 8) | const DEFAULT_CACHE_DURATION = 5 * 60 * 1000;
  class Lib (line 9) | class Lib {
    method constructor (line 10) | constructor(projectID, options) {
    method captureError (line 24) | captureError(event) {
    method trackErrors (line 45) | trackErrors(options) {
    method submitError (line 59) | submitError(payload, evokeCallback) {
    method track (line 85) | async track(event) {
    method trackPageViews (line 109) | trackPageViews(options) {
    method getPerformanceStats (line 139) | getPerformanceStats() {
    method getFeatureFlags (line 172) | async getFeatureFlags(options, forceRefresh) {
    method fetchFlagsAndExperiments (line 197) | async fetchFlagsAndExperiments(options) {
    method getFeatureFlag (line 237) | async getFeatureFlag(key, options, defaultValue = false) {
    method clearFeatureFlagsCache (line 244) | clearFeatureFlagsCache() {
    method getExperiments (line 261) | async getExperiments(options, forceRefresh) {
    method getExperiment (line 302) | async getExperiment(experimentId, options, defaultVariant = null) {
    method clearExperimentsCache (line 309) | clearExperimentsCache() {
    method getProfileId (line 335) | async getProfileId() {
    method getSessionId (line 384) | async getSessionId() {
    method getApiBase (line 410) | getApiBase() {
    method heartbeat (line 417) | heartbeat() {
    method trackPathChange (line 430) | trackPathChange() {
    method trackPage (line 442) | trackPage(pg, unique = false) {
    method submitPageView (line 450) | submitPageView(payload, unique, perf, evokeCallback) {
    method canTrack (line 480) | canTrack() {
    method sendRequest (line 490) | async sendRequest(path, body) {

FILE: dist/esnext/index.d.ts
  type IPageviewOptions (line 52) | interface IPageviewOptions {

FILE: dist/esnext/index.js
  constant LIB_INSTANCE (line 2) | let LIB_INSTANCE = null;
  function init (line 10) | function init(pid, options) {
  function track (line 23) | async function track(event) {
  function trackViews (line 34) | function trackViews(options) {
  function trackErrors (line 58) | function trackErrors(options) {
  function trackError (line 71) | function trackError(payload) {
  function trackPageview (line 86) | function trackPageview(pg, _prev, unique) {
  function pageview (line 91) | function pageview(options) {
  function getFeatureFlags (line 116) | async function getFeatureFlags(options, forceRefresh) {
  function getFeatureFlag (line 138) | async function getFeatureFlag(key, options, defaultValue = false) {
  function clearFeatureFlagsCache (line 147) | function clearFeatureFlagsCache() {
  function getExperiments (line 174) | async function getExperiments(options, forceRefresh) {
  function getExperiment (line 203) | async function getExperiment(experimentId, options, defaultVariant = nul...
  function clearExperimentsCache (line 212) | function clearExperimentsCache() {
  function getProfileId (line 240) | async function getProfileId() {
  function getSessionId (line 267) | async function getSessionId() {

FILE: dist/esnext/utils.d.ts
  type IGetPath (line 1) | interface IGetPath {

FILE: dist/swetrix.cjs.js
  method stop (line 74) | stop() { }
  constant DEFAULT_API_HOST (line 76) | const DEFAULT_API_HOST = 'https://api.swetrix.com/log';
  constant DEFAULT_API_BASE (line 77) | const DEFAULT_API_BASE = 'https://api.swetrix.com';
  constant DEFAULT_CACHE_DURATION (line 79) | const DEFAULT_CACHE_DURATION = 5 * 60 * 1000;
  class Lib (line 80) | class Lib {
    method constructor (line 81) | constructor(projectID, options) {
    method captureError (line 95) | captureError(event) {
    method trackErrors (line 117) | trackErrors(options) {
    method submitError (line 131) | submitError(payload, evokeCallback) {
    method track (line 158) | async track(event) {
    method trackPageViews (line 183) | trackPageViews(options) {
    method getPerformanceStats (line 213) | getPerformanceStats() {
    method getFeatureFlags (line 247) | async getFeatureFlags(options, forceRefresh) {
    method fetchFlagsAndExperiments (line 273) | async fetchFlagsAndExperiments(options) {
    method getFeatureFlag (line 314) | async getFeatureFlag(key, options, defaultValue = false) {
    method clearFeatureFlagsCache (line 322) | clearFeatureFlagsCache() {
    method getExperiments (line 339) | async getExperiments(options, forceRefresh) {
    method getExperiment (line 381) | async getExperiment(experimentId, options, defaultVariant = null) {
    method clearExperimentsCache (line 389) | clearExperimentsCache() {
    method getProfileId (line 415) | async getProfileId() {
    method getSessionId (line 465) | async getSessionId() {
    method getApiBase (line 491) | getApiBase() {
    method heartbeat (line 499) | heartbeat() {
    method trackPathChange (line 513) | trackPathChange() {
    method trackPage (line 526) | trackPage(pg, unique = false) {
    method submitPageView (line 534) | submitPageView(payload, unique, perf, evokeCallback) {
    method canTrack (line 565) | canTrack() {
    method sendRequest (line 576) | async sendRequest(path, body) {
  function init (line 597) | function init(pid, options) {
  function track (line 610) | async function track(event) {
  function trackViews (line 621) | function trackViews(options) {
  function trackErrors (line 645) | function trackErrors(options) {
  function trackError (line 658) | function trackError(payload) {
  function trackPageview (line 673) | function trackPageview(pg, _prev, unique) {
  function pageview (line 678) | function pageview(options) {
  function getFeatureFlags (line 703) | async function getFeatureFlags(options, forceRefresh) {
  function getFeatureFlag (line 725) | async function getFeatureFlag(key, options, defaultValue = false) {
  function clearFeatureFlagsCache (line 734) | function clearFeatureFlagsCache() {
  function getExperiments (line 761) | async function getExperiments(options, forceRefresh) {
  function getExperiment (line 790) | async function getExperiment(experimentId, options, defaultVariant = nul...
  function clearExperimentsCache (line 799) | function clearExperimentsCache() {
  function getProfileId (line 827) | async function getProfileId() {
  function getSessionId (line 854) | async function getSessionId() {

FILE: dist/swetrix.es5.js
  method stop (line 72) | stop() { }
  constant DEFAULT_API_HOST (line 74) | const DEFAULT_API_HOST = 'https://api.swetrix.com/log';
  constant DEFAULT_API_BASE (line 75) | const DEFAULT_API_BASE = 'https://api.swetrix.com';
  constant DEFAULT_CACHE_DURATION (line 77) | const DEFAULT_CACHE_DURATION = 5 * 60 * 1000;
  class Lib (line 78) | class Lib {
    method constructor (line 79) | constructor(projectID, options) {
    method captureError (line 93) | captureError(event) {
    method trackErrors (line 115) | trackErrors(options) {
    method submitError (line 129) | submitError(payload, evokeCallback) {
    method track (line 156) | async track(event) {
    method trackPageViews (line 181) | trackPageViews(options) {
    method getPerformanceStats (line 211) | getPerformanceStats() {
    method getFeatureFlags (line 245) | async getFeatureFlags(options, forceRefresh) {
    method fetchFlagsAndExperiments (line 271) | async fetchFlagsAndExperiments(options) {
    method getFeatureFlag (line 312) | async getFeatureFlag(key, options, defaultValue = false) {
    method clearFeatureFlagsCache (line 320) | clearFeatureFlagsCache() {
    method getExperiments (line 337) | async getExperiments(options, forceRefresh) {
    method getExperiment (line 379) | async getExperiment(experimentId, options, defaultVariant = null) {
    method clearExperimentsCache (line 387) | clearExperimentsCache() {
    method getProfileId (line 413) | async getProfileId() {
    method getSessionId (line 463) | async getSessionId() {
    method getApiBase (line 489) | getApiBase() {
    method heartbeat (line 497) | heartbeat() {
    method trackPathChange (line 511) | trackPathChange() {
    method trackPage (line 524) | trackPage(pg, unique = false) {
    method submitPageView (line 532) | submitPageView(payload, unique, perf, evokeCallback) {
    method canTrack (line 563) | canTrack() {
    method sendRequest (line 574) | async sendRequest(path, body) {
  constant LIB_INSTANCE (line 587) | let LIB_INSTANCE = null;
  function init (line 595) | function init(pid, options) {
  function track (line 608) | async function track(event) {
  function trackViews (line 619) | function trackViews(options) {
  function trackErrors (line 643) | function trackErrors(options) {
  function trackError (line 656) | function trackError(payload) {
  function trackPageview (line 671) | function trackPageview(pg, _prev, unique) {
  function pageview (line 676) | function pageview(options) {
  function getFeatureFlags (line 701) | async function getFeatureFlags(options, forceRefresh) {
  function getFeatureFlag (line 723) | async function getFeatureFlag(key, options, defaultValue = false) {
  function clearFeatureFlagsCache (line 732) | function clearFeatureFlagsCache() {
  function getExperiments (line 759) | async function getExperiments(options, forceRefresh) {
  function getExperiment (line 788) | async function getExperiment(experimentId, options, defaultVariant = nul...
  function clearExperimentsCache (line 797) | function clearExperimentsCache() {
  function getProfileId (line 825) | async function getProfileId() {
  function getSessionId (line 852) | async function getSessionId() {

FILE: dist/swetrix.js
  method stop (line 1) | stop(){}
  class N (line 1) | class N{constructor(t,e){this.projectID=t,this.options=e,this.pageData=n...
    method constructor (line 1) | constructor(t,e){this.projectID=t,this.options=e,this.pageData=null,th...
    method captureError (line 1) | captureError(t){var e,i,a,n;"number"==typeof(null===(e=this.errorsOpti...
    method trackErrors (line 1) | trackErrors(t){return this.errorListenerExists||!this.canTrack()?m:(th...
    method submitError (line 1) | submitError(t,e){var i,a,n;const o={pid:this.projectID},r={pg:this.act...
    method track (line 1) | async track(t){var e,i,a,n;if(!this.canTrack())return;const o={...t,pi...
    method trackPageViews (line 1) | trackPageViews(t){if(!this.canTrack())return m;if(this.pageData)return...
    method getPerformanceStats (line 1) | getPerformanceStats(){var t;if(!this.canTrack()||this.perfStatsCollect...
    method getFeatureFlags (line 1) | async getFeatureFlags(t,e){var i,a,n,o;if(!l())return{};const r=null!=...
    method fetchFlagsAndExperiments (line 1) | async fetchFlagsAndExperiments(t){var e,i,a,n;const o=this.getApiBase(...
    method getFeatureFlag (line 1) | async getFeatureFlag(t,e,i=!1){var a;return null!==(a=(await this.getF...
    method clearFeatureFlagsCache (line 1) | clearFeatureFlagsCache(){this.cachedData=null}
    method getExperiments (line 1) | async getExperiments(t,e){var i,a,n,o;if(!l())return{};const r=null!==...
    method getExperiment (line 1) | async getExperiment(t,e,i=null){var a;return null!==(a=(await this.get...
    method clearExperimentsCache (line 1) | clearExperimentsCache(){this.cachedData=null}
    method getProfileId (line 1) | async getProfileId(){var t;if(null===(t=this.options)||void 0===t?void...
    method getSessionId (line 1) | async getSessionId(){if(!l())return null;try{const t=this.getApiBase()...
    method getApiBase (line 1) | getApiBase(){var t;return(null===(t=this.options)||void 0===t?void 0:t...
    method heartbeat (line 1) | heartbeat(){var t,e;if(!(null===(t=this.pageViewsOptions)||void 0===t?...
    method trackPathChange (line 1) | trackPathChange(){var t,e;if(!this.pageData)return;const i=I({hash:nul...
    method trackPage (line 1) | trackPage(t,e=!1){if(!this.pageData)return;this.pageData.path=t;const ...
    method submitPageView (line 1) | submitPageView(t,e,i,a){var n,o;const r={pid:this.projectID,perf:i,uni...
    method canTrack (line 1) | canTrack(){var t,e,i,a;return!((null===(t=this.options)||void 0===t?vo...
    method sendRequest (line 1) | async sendRequest(t,e){var i;const a=(null===(i=this.options)||void 0=...

FILE: src/Lib.ts
  type LibOptions (line 16) | interface LibOptions {
  type TrackEventOptions (line 43) | interface TrackEventOptions {
  type IPageViewPayload (line 60) | interface IPageViewPayload {
  type IErrorEventPayload (line 81) | interface IErrorEventPayload {
  type IInternalErrorEventPayload (line 93) | interface IInternalErrorEventPayload extends IErrorEventPayload {
  type IPerfPayload (line 99) | interface IPerfPayload {
  type FeatureFlagsOptions (line 113) | interface FeatureFlagsOptions {
  type ExperimentOptions (line 125) | interface ExperimentOptions {
  type CachedData (line 137) | interface CachedData {
  type PageActions (line 148) | interface PageActions {
  type ErrorActions (line 156) | interface ErrorActions {
  type PageData (line 161) | interface PageData {
  type ErrorOptions (line 169) | interface ErrorOptions {
  type PageViewsOptions (line 188) | interface PageViewsOptions {
  method stop (line 220) | stop() {}
  constant DEFAULT_API_HOST (line 223) | const DEFAULT_API_HOST = 'https://api.swetrix.com/log'
  constant DEFAULT_API_BASE (line 224) | const DEFAULT_API_BASE = 'https://api.swetrix.com'
  constant DEFAULT_CACHE_DURATION (line 227) | const DEFAULT_CACHE_DURATION = 5 * 60 * 1000
  class Lib (line 229) | class Lib {
    method constructor (line 238) | constructor(private projectID: string, private options?: LibOptions) {
    method captureError (line 244) | captureError(event: ErrorEvent): void {
    method trackErrors (line 275) | trackErrors(options?: ErrorOptions): ErrorActions {
    method submitError (line 293) | submitError(payload: IErrorEventPayload, evokeCallback?: boolean): void {
    method track (line 327) | async track(event: TrackEventOptions): Promise<void> {
    method trackPageViews (line 354) | trackPageViews(options?: PageViewsOptions): PageActions {
    method getPerformanceStats (line 392) | getPerformanceStats(): IPerfPayload | {} {
    method getFeatureFlags (line 432) | async getFeatureFlags(options?: FeatureFlagsOptions, forceRefresh?: bo...
    method fetchFlagsAndExperiments (line 460) | private async fetchFlagsAndExperiments(options?: FeatureFlagsOptions |...
    method getFeatureFlag (line 510) | async getFeatureFlag(key: string, options?: FeatureFlagsOptions, defau...
    method clearFeatureFlagsCache (line 518) | clearFeatureFlagsCache(): void {
    method getExperiments (line 536) | async getExperiments(options?: ExperimentOptions, forceRefresh?: boole...
    method getExperiment (line 580) | async getExperiment(
    method clearExperimentsCache (line 592) | clearExperimentsCache(): void {
    method getProfileId (line 619) | async getProfileId(): Promise<string | null> {
    method getSessionId (line 672) | async getSessionId(): Promise<string | null> {
    method getApiBase (line 701) | private getApiBase(): string {
    method heartbeat (line 709) | private heartbeat(): void {
    method trackPathChange (line 726) | private trackPathChange(): void {
    method trackPage (line 739) | private trackPage(pg: string, unique: boolean = false): void {
    method submitPageView (line 749) | submitPageView(
    method canTrack (line 791) | private canTrack(): boolean {
    method sendRequest (line 805) | private async sendRequest(path: string, body: object): Promise<void> {

FILE: src/index.ts
  constant LIB_INSTANCE (line 16) | let LIB_INSTANCE: Lib | null = null
  function init (line 25) | function init(pid: string, options?: LibOptions): Lib {
  function track (line 40) | async function track(event: TrackEventOptions): Promise<void> {
  function trackViews (line 52) | function trackViews(options?: PageViewsOptions): Promise<PageActions> {
  function trackErrors (line 77) | function trackErrors(options?: ErrorOptions): ErrorActions {
  function trackError (line 92) | function trackError(payload: IErrorEventPayload): void {
  function trackPageview (line 108) | function trackPageview(pg: string, _prev?: string, unique?: boolean): vo...
  type IPageviewOptions (line 114) | interface IPageviewOptions {
  function pageview (line 119) | function pageview(options: IPageviewOptions): void {
  function getFeatureFlags (line 145) | async function getFeatureFlags(
  function getFeatureFlag (line 171) | async function getFeatureFlag(
  function clearFeatureFlagsCache (line 185) | function clearFeatureFlagsCache(): void {
  function getExperiments (line 213) | async function getExperiments(
  function getExperiment (line 246) | async function getExperiment(
  function clearExperimentsCache (line 260) | function clearExperimentsCache(): void {
  function getProfileId (line 289) | async function getProfileId(): Promise<string | null> {
  function getSessionId (line 317) | async function getSessionId(): Promise<string | null> {

FILE: src/utils.ts
  type IGetPath (line 1) | interface IGetPath {

FILE: tests/errors.test.ts
  method trackErrors (line 56) | trackErrors(options?: any) {
Condensed preview — 30 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (209K chars).
[
  {
    "path": ".github/funding.yml",
    "chars": 31,
    "preview": "github: swetrix\nko_fi: andriir\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 595,
    "preview": "name: Tests\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n    types: [opened, labeled, synchron"
  },
  {
    "path": ".gitignore",
    "chars": 1648,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs."
  },
  {
    "path": ".prettierrc.cjs",
    "chars": 854,
    "preview": "/**\n * @see https://prettier.io/docs/configuration\n * @type {import(\"prettier\").Config}\n */\nconst config = {\n  printWidt"
  },
  {
    "path": "LICENSE",
    "chars": 1072,
    "preview": "MIT License\n\nCopyright (c) 2021 Andrii Romasiun\n\nPermission is hereby granted, free of charge, to any person obtaining a"
  },
  {
    "path": "README.md",
    "chars": 1838,
    "preview": "<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://swetrix.com/assets/logo_white.png\">\n  <img alt="
  },
  {
    "path": "dist/esnext/Lib.d.ts",
    "chars": 10957,
    "preview": "export interface LibOptions {\n    /**\n     * When set to `true`, localhost events will be sent to server.\n     */\n    de"
  },
  {
    "path": "dist/esnext/Lib.js",
    "chars": 18383,
    "preview": "import { isInBrowser, isLocalhost, isAutomated, getLocale, getTimezone, getReferrer, getUTMCampaign, getUTMMedium, getUT"
  },
  {
    "path": "dist/esnext/index.d.ts",
    "chars": 8060,
    "preview": "import { Lib, LibOptions, TrackEventOptions, PageViewsOptions, ErrorOptions, PageActions, ErrorActions, IErrorEventPaylo"
  },
  {
    "path": "dist/esnext/index.js",
    "chars": 9088,
    "preview": "import { Lib, defaultActions, } from './Lib.js';\nexport let LIB_INSTANCE = null;\n/**\n * Initialise the tracking library "
  },
  {
    "path": "dist/esnext/utils.d.ts",
    "chars": 1186,
    "preview": "interface IGetPath {\n    hash?: boolean;\n    search?: boolean;\n}\nexport declare const isInBrowser: () => boolean;\nexport"
  },
  {
    "path": "dist/esnext/utils.js",
    "chars": 2569,
    "preview": "const findInSearch = (exp) => {\n    const res = location.search.match(exp);\n    return (res && res[2]) || undefined;\n};\n"
  },
  {
    "path": "dist/swetrix.cjs.js",
    "chars": 33106,
    "preview": "'use strict';\n\nconst findInSearch = (exp) => {\n    const res = location.search.match(exp);\n    return (res && res[2]) ||"
  },
  {
    "path": "dist/swetrix.es5.js",
    "chars": 32507,
    "preview": "const findInSearch = (exp) => {\n    const res = location.search.match(exp);\n    return (res && res[2]) || undefined;\n};\n"
  },
  {
    "path": "dist/swetrix.js",
    "chars": 10612,
    "preview": "!function(t,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?e(exports):\"function\"==typeof define&&define.amd?def"
  },
  {
    "path": "jest.config.js",
    "chars": 268,
    "preview": "export default {\n  preset: 'ts-jest',\n  testEnvironment: 'jsdom',\n  extensionsToTreatAsEsm: ['.ts'],\n  moduleNameMapper:"
  },
  {
    "path": "package.json",
    "chars": 1832,
    "preview": "{\n  \"name\": \"swetrix\",\n  \"version\": \"4.1.0\",\n  \"description\": \"The JavaScript analytics client for Swetrix Analytics\",\n "
  },
  {
    "path": "rollup.config.mjs",
    "chars": 1042,
    "preview": "import { nodeResolve } from '@rollup/plugin-node-resolve'\nimport commonjs from '@rollup/plugin-commonjs'\nimport typescri"
  },
  {
    "path": "src/Lib.ts",
    "chars": 22902,
    "preview": "import {\n  isInBrowser,\n  isLocalhost,\n  isAutomated,\n  getLocale,\n  getTimezone,\n  getReferrer,\n  getUTMCampaign,\n  get"
  },
  {
    "path": "src/index.ts",
    "chars": 9861,
    "preview": "import {\n  Lib,\n  LibOptions,\n  TrackEventOptions,\n  PageViewsOptions,\n  ErrorOptions,\n  PageActions,\n  ErrorActions,\n  "
  },
  {
    "path": "src/utils.ts",
    "chars": 2572,
    "preview": "interface IGetPath {\n  hash?: boolean\n  search?: boolean\n}\n\nconst findInSearch = (exp: RegExp): string | undefined => {\n"
  },
  {
    "path": "tests/README.md",
    "chars": 1313,
    "preview": "# Swetrix JS Tests\n\nThis directory contains tests for the Swetrix JavaScript analytics client.\n\n## Test Structure\n\n- **i"
  },
  {
    "path": "tests/errors.test.ts",
    "chars": 6305,
    "preview": "import { init, trackError, trackErrors } from '../src/index'\nimport { Lib } from '../src/Lib'\n\njest.mock('../src/Lib', ("
  },
  {
    "path": "tests/events.test.ts",
    "chars": 2686,
    "preview": "import { init, track } from '../src/index'\nimport { Lib } from '../src/Lib'\n\njest.mock('../src/Lib', () => {\n  const ori"
  },
  {
    "path": "tests/experiments.test.ts",
    "chars": 9064,
    "preview": "import { init, getExperiment, getExperiments, clearExperimentsCache } from '../src/index'\nimport { Lib } from '../src/Li"
  },
  {
    "path": "tests/initialisation.test.ts",
    "chars": 2214,
    "preview": "import { init } from '../src/index'\nimport { Lib } from '../src/Lib'\n\njest.mock('../src/Lib', () => {\n  const originalMo"
  },
  {
    "path": "tests/pageview.test.ts",
    "chars": 2838,
    "preview": "import { init, pageview, trackViews } from '../src/index'\nimport { Lib } from '../src/Lib'\n\njest.mock('../src/Lib', () ="
  },
  {
    "path": "tests/utils.test.ts",
    "chars": 5522,
    "preview": "import * as utils from '../src/utils'\n\ndescribe('Utility Functions', () => {\n  beforeEach(() => {\n    Object.definePrope"
  },
  {
    "path": "tsconfig.esnext.json",
    "chars": 301,
    "preview": "{\n  \"compilerOptions\": {\n    \"moduleResolution\": \"node\",\n    \"target\": \"ES2020\",\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES"
  },
  {
    "path": "tsconfig.json",
    "chars": 273,
    "preview": "{\n  \"compilerOptions\": {\n    \"moduleResolution\": \"node\",\n    \"target\": \"ES2018\",\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES"
  }
]

About this extraction

This page contains the full source code of the Swetrix/swetrix-js GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 30 files (196.8 KB), approximately 48.7k tokens, and a symbol index with 233 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!