tiny-utm-keeper
Version:
Zero-dependency, TypeScript-first UTM parameter tracker for first-touch/last-touch attribution. SSR-friendly.
190 lines (189 loc) • 5.34 kB
JavaScript
import { getStoredUTMData, saveUTMData, clearUTMData, isBrowser, } from './storage.js';
import { extractUTMsFromURL, extractUTMsFromCurrentURL, appendUTMsToURL, hasUTMParams, utmParamsToObject, } from './utils.js';
/**
* Default configuration
*/
const DEFAULT_CONFIG = {
mode: 'first-touch',
expirationDays: 30,
storageKey: 'utm_keeper',
autoCapture: true,
};
/**
* UTMKeeper class - Main API
*/
export class UTMKeeper {
constructor(config = {}) {
this.config = {
...DEFAULT_CONFIG,
...config,
};
// Auto-capture on init if enabled
if (this.config.autoCapture && isBrowser()) {
this.capture(config.captureUrl);
}
}
/**
* Capture UTM parameters from URL
*/
capture(url) {
if (!isBrowser()) {
return false;
}
const utmParams = url
? extractUTMsFromURL(url)
: extractUTMsFromCurrentURL();
if (!hasUTMParams(utmParams)) {
return false;
}
// Check attribution mode
if (this.config.mode === 'first-touch') {
const existing = getStoredUTMData(this.config.storageKey);
if (existing && hasUTMParams(existing.params)) {
// Already have first-touch data, don't override
return false;
}
}
// Save UTM data (for both first-touch and last-touch)
return saveUTMData(this.config.storageKey, utmParams, this.config.expirationDays);
}
/**
* Get stored UTM parameters
*/
getUTMs() {
if (!isBrowser()) {
return null;
}
const data = getStoredUTMData(this.config.storageKey);
return data ? data.params : null;
}
/**
* Get stored UTM data with metadata
*/
getUTMData() {
if (!isBrowser()) {
return null;
}
return getStoredUTMData(this.config.storageKey);
}
/**
* Clear stored UTM parameters
*/
clear() {
return clearUTMData(this.config.storageKey);
}
/**
* Append stored UTMs to a URL
*/
appendToURL(url) {
const utms = this.getUTMs();
if (!hasUTMParams(utms)) {
return url;
}
return appendUTMsToURL(url, utms);
}
/**
* Get UTM parameters as object for API calls
*/
getUTMObject() {
const utms = this.getUTMs();
if (!hasUTMParams(utms)) {
return {};
}
return utmParamsToObject(utms);
}
/**
* Create a fetch wrapper that automatically injects UTMs
*/
createFetch() {
const originalFetch = fetch;
const self = this;
return function utmFetch(input, init) {
// Skip if explicitly disabled or not in browser
if (!isBrowser() || init?.skipUTM) {
const { skipUTM, ...cleanInit } = init || {};
return originalFetch(input, cleanInit);
}
const utms = self.getUTMs();
if (!hasUTMParams(utms)) {
return originalFetch(input, init);
}
// Handle URL injection
let modifiedInput = input;
if (typeof input === 'string') {
modifiedInput = appendUTMsToURL(input, utms);
}
else if (input instanceof URL) {
modifiedInput = new URL(appendUTMsToURL(input.toString(), utms));
}
else if (input instanceof Request) {
modifiedInput = new Request(appendUTMsToURL(input.url, utms), input);
}
return originalFetch(modifiedInput, init);
};
}
/**
* Update configuration
*/
updateConfig(config) {
this.config = {
...this.config,
...config,
};
}
/**
* Get current configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Check if UTMs are currently stored
*/
hasStoredUTMs() {
return hasUTMParams(this.getUTMs());
}
/**
* Get attribution mode
*/
getMode() {
return this.config.mode;
}
/**
* Set attribution mode
*/
setMode(mode) {
this.config.mode = mode;
}
}
/**
* Create a singleton instance for convenience
*/
let defaultInstance = null;
/**
* Initialize the default UTMKeeper instance
*/
export const init = (config = {}) => {
defaultInstance = new UTMKeeper(config);
return defaultInstance;
};
/**
* Get the default instance (creates one if it doesn't exist)
*/
export const getInstance = () => {
if (!defaultInstance) {
defaultInstance = new UTMKeeper();
}
return defaultInstance;
};
/**
* Convenience functions using the default instance
*/
export const capture = (url) => getInstance().capture(url);
export const getUTMs = () => getInstance().getUTMs();
export const getUTMData = () => getInstance().getUTMData();
export const clear = () => getInstance().clear();
export const appendToURL = (url) => getInstance().appendToURL(url);
export const getUTMObject = () => getInstance().getUTMObject();
export const createFetch = () => getInstance().createFetch();
export const hasStoredUTMs = () => getInstance().hasStoredUTMs();