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
================================================
> [!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.
[](https://data.jsdelivr.com/v1/package/gh/Swetrix/swetrix-js/stats)
[](https://bundlephobia.com/api/size?package=swetrix)
[](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 | 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 | 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;
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>;
/**
* 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;
/**
* 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>;
/**
* 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;
/**
* 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;
/**
* 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;
/**
* Gets the API base URL (without /log suffix).
*/
private getApiBase;
private heartbeat;
private trackPathChange;
private trackPage;
submitPageView(payload: Partial, 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;
/**
* 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;
/**
* 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;
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>;
/**
* 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;
/**
* 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>;
/**
* 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;
/**
* 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;
/**
* 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;
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) ? '' : 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) ? '' : 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) ? '' : 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)?"":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{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. ",
"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
experiments: Record
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 | 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 | 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 {
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> {
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 {
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
experiments?: Record
}
// 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 {
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> {
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 {
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 {
// 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 {
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,
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 {
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 {
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 {
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
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> {
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 {
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> {
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 {
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 {
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 {
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) ? '' : 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()).toBe(true)
// Test 127.0.0.1
Object.defineProperty(window, 'location', {
value: {
hostname: '127.0.0.1',
},
writable: true,
})
expect(utils.isLocalhost()).toBe(true)
// Test non-localhost
Object.defineProperty(window, 'location', {
value: {
hostname: 'example.com',
},
writable: true,
})
expect(utils.isLocalhost()).toBe(false)
})
test('isAutomated should detect webdriver', () => {
// Arrange
Object.defineProperty(navigator, 'webdriver', {
value: true,
writable: true,
})
// Act & Assert
expect(utils.isAutomated()).toBe(true)
// Test non-automated
Object.defineProperty(navigator, 'webdriver', {
value: false,
writable: true,
})
expect(utils.isAutomated()).toBe(false)
})
test('getLocale should return the browser language', () => {
expect(utils.getLocale()).toBe('en-US')
// Test with navigator.languages undefined
const originalLanguages = navigator.languages
// Instead of deleting, set to undefined
Object.defineProperty(navigator, 'languages', {
value: undefined,
writable: true,
})
expect(utils.getLocale()).toBe('en-US') // Should use navigator.language as fallback
// Restore the original value
Object.defineProperty(navigator, 'languages', {
value: originalLanguages,
writable: true,
})
})
test('getReferrer should return the document referrer', () => {
expect(utils.getReferrer()).toBe('https://google.com')
// Test with empty referrer
Object.defineProperty(document, 'referrer', {
value: '',
writable: true,
})
expect(utils.getReferrer()).toBeUndefined()
})
test('getPath should handle different URL formats', () => {
// Arrange
Object.defineProperty(window, 'location', {
value: {
pathname: '/test-page',
hash: '',
search: '',
},
writable: true,
})
// Act & Assert - basic path
expect(utils.getPath({})).toBe('/test-page')
// Test with hash
Object.defineProperty(window, 'location', {
value: {
pathname: '/test-page',
hash: '#section1',
search: '',
},
writable: true,
})
expect(utils.getPath({ hash: true })).toBe('/test-page#section1')
// Test with search
Object.defineProperty(window, 'location', {
value: {
pathname: '/test-page',
hash: '',
search: '?param=value',
},
writable: true,
})
expect(utils.getPath({ search: true })).toBe('/test-page?param=value')
// Test with both hash and search
Object.defineProperty(window, 'location', {
value: {
pathname: '/test-page',
hash: '#section1',
search: '?param=value',
},
writable: true,
})
expect(utils.getPath({ hash: true, search: true })).toBe('/test-page#section1?param=value')
// Test with search in hash
Object.defineProperty(window, 'location', {
value: {
pathname: '/test-page',
hash: '#section1?param=value',
search: '',
},
writable: true,
})
expect(utils.getPath({ hash: true, search: true })).toBe('/test-page#section1?param=value')
})
test('getUTM* functions should extract UTM parameters', () => {
// Arrange
Object.defineProperty(window, 'location', {
value: {
pathname: '/landing',
hash: '',
search: '?utm_source=google&utm_medium=cpc&utm_campaign=summer&utm_term=analytics&utm_content=ad1',
},
writable: true,
})
// Act & Assert
expect(utils.getUTMSource()).toBe('google')
expect(utils.getUTMMedium()).toBe('cpc')
expect(utils.getUTMCampaign()).toBe('summer')
expect(utils.getUTMTerm()).toBe('analytics')
expect(utils.getUTMContent()).toBe('ad1')
// Test with 'ref' and 'source' parameters as well
Object.defineProperty(window, 'location', {
value: {
pathname: '/landing',
hash: '',
search: '?ref=twitter',
},
writable: true,
})
expect(utils.getUTMSource()).toBe('twitter')
Object.defineProperty(window, 'location', {
value: {
pathname: '/landing',
hash: '',
search: '?source=newsletter',
},
writable: true,
})
expect(utils.getUTMSource()).toBe('newsletter')
})
})
================================================
FILE: tsconfig.esnext.json
================================================
{
"compilerOptions": {
"moduleResolution": "node",
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"strict": true,
"sourceMap": true,
"declaration": true,
"outDir": "dist/esnext",
"typeRoots": ["node_modules/@types"]
},
"include": ["src"]
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"moduleResolution": "node",
"target": "ES2018",
"module": "ESNext",
"lib": ["ES2018", "DOM"],
"strict": true,
"sourceMap": true,
"declaration": false,
"allowSyntheticDefaultImports": true
},
"include": ["src"]
}