A comprehensive TypeScript library for capturing, storing, and appending UTM tracking parameters. Framework-agnostic core with optional React integration.
- Capture UTM parameters from URLs
- Sanitize parameter values to prevent XSS and injection
- PII filtering to detect and reject/redact email addresses, phone numbers, and other PII
- Store in sessionStorage or localStorage (with optional TTL)
- Attribution — first-touch, last-touch, or both
- Form population — inject UTM data into HTML forms (vanilla JS + React)
- Append UTM parameters to share URLs
- Build UTM-tagged URLs with validation and warnings
- Link decoration — auto-append UTM params to links on a page (vanilla JS + React)
- Event callbacks — lifecycle hooks for capture, store, clear, append, and expiry events
- Configurable key format (snake_case or camelCase)
- Platform-specific share context parameters
- Fragment mode support (add params to
#hashinstead of?query) - URL validation and normalization
- React hook and context provider
- Debug utilities for troubleshooting
- SSR-safe with graceful fallbacks
- Zero dependencies (peer dependency on React is optional)
npm install @jackmisner/utm-toolkitimport {
captureUtmParameters,
storeUtmParameters,
getStoredUtmParameters,
appendUtmParameters,
} from '@jackmisner/utm-toolkit';
// Capture UTM params from URL
// URL: https://example.com?utm_source=linkedin&utm_campaign=spring2025
const params = captureUtmParameters();
// { utm_source: 'linkedin', utm_campaign: 'spring2025' }
// Store for the session
storeUtmParameters(params);
// Later, retrieve stored params
const stored = getStoredUtmParameters();
// Append to a share URL
const shareUrl = appendUtmParameters('https://example.com/share', stored);
// https://example.com/share?utm_source=linkedin&utm_campaign=spring2025import { useUtmTracking } from '@jackmisner/utm-toolkit/react';
function ShareButton() {
const { appendToUrl, hasParams } = useUtmTracking();
const handleShare = () => {
const shareUrl = appendToUrl('https://example.com/results', 'linkedin');
window.open(`https://linkedin.com/share?url=${encodeURIComponent(shareUrl)}`);
};
return <button onClick={handleShare}>Share on LinkedIn</button>;
}import { UtmProvider, useUtmContext } from '@jackmisner/utm-toolkit/react';
// Wrap your app
function App() {
return (
<UtmProvider config={{ storageKey: 'myapp_utm' }}>
<MyComponent />
</UtmProvider>
);
}
// Access UTM state anywhere
function MyComponent() {
const { utmParameters, appendToUrl } = useUtmContext();
// ...
}Extract UTM parameters from a URL.
// Capture from current page URL
const params = captureUtmParameters();
// Capture from specific URL
const params = captureUtmParameters('https://example.com?utm_source=test');
// With options
const params = captureUtmParameters(url, {
keyFormat: 'camelCase', // 'snake_case' (default) or 'camelCase'
allowedParameters: ['utm_source', 'utm_campaign'], // Filter to specific params
});
// With sanitization (strips HTML, control chars)
const params = captureUtmParameters(url, {
sanitize: {
enabled: true,
stripHtml: true, // Remove < > " ' ` (default: true)
stripControlChars: true, // Remove control characters (default: true)
maxLength: 200, // Truncate values (default: 200)
},
});Store UTM parameters in browser storage.
storeUtmParameters({ utm_source: 'linkedin', utm_campaign: 'sale' });
// With custom storage key
storeUtmParameters(params, { storageKey: 'myapp_utm' });
// Store in localStorage (persists across sessions)
storeUtmParameters(params, { storageType: 'local' });
// Store in localStorage with 1-hour TTL
storeUtmParameters(params, { storageType: 'local', ttl: 3600000 });
// Store in camelCase format
storeUtmParameters(params, { keyFormat: 'camelCase' });Retrieve stored UTM parameters. Returns null if data has expired (when TTL was set).
const params = getStoredUtmParameters();
// Read from localStorage
const params = getStoredUtmParameters({ storageType: 'local' });
// With options
const params = getStoredUtmParameters({
storageKey: 'myapp_utm',
keyFormat: 'camelCase', // Convert to camelCase on retrieval
});Append UTM parameters to a URL.
// Basic usage
const url = appendUtmParameters('https://example.com', { utm_source: 'test' });
// With options
const url = appendUtmParameters(url, params, {
toFragment: true, // Add to #hash instead of ?query
preserveExisting: true, // Don't replace existing UTM params
});Clear stored UTM parameters.
clearStoredUtmParameters();
clearStoredUtmParameters({ storageKey: 'myapp_utm' });
clearStoredUtmParameters({ storageType: 'local' });
clearStoredUtmParameters({ onClear: () => console.log('Cleared!') });
// Legacy positional args still work
clearStoredUtmParameters('myapp_utm');
clearStoredUtmParameters('utm_parameters', 'local');import {
toSnakeCase,
toCamelCase,
convertParams,
} from '@jackmisner/utm-toolkit';
// Convert single keys
toSnakeCase('utmSource'); // 'utm_source'
toCamelCase('utm_source'); // 'utmSource'
// Convert entire objects
convertParams({ utmSource: 'test' }, 'snake_case');
// { utm_source: 'test' }import {
validateUrl,
normalizeUrl,
validateAndNormalize,
} from '@jackmisner/utm-toolkit';
// Validate URL
const result = validateUrl('https://example.com');
// { valid: true }
const result = validateUrl('ftp://example.com');
// { valid: false, error: 'invalid_protocol', message: '...' }
// Normalize URL (add protocol if missing)
normalizeUrl('example.com'); // 'https://example.com'
// Combined
validateAndNormalize('example.com');
// { valid: true, normalizedUrl: 'https://example.com' }Sanitize UTM parameter values to prevent XSS when rendering in HTML or constructing URLs. Sanitization is disabled by default and runs at capture time only.
import { captureUtmParameters, sanitizeValue, sanitizeParams } from '@jackmisner/utm-toolkit';
// Enable sanitization during capture
const params = captureUtmParameters('https://example.com?utm_source=<script>bad</script>', {
sanitize: { enabled: true },
});
// { utm_source: 'scriptbad/script' }
// Use standalone sanitization functions
sanitizeValue('<b>bold</b>', {
enabled: true,
stripHtml: true,
stripControlChars: true,
maxLength: 200,
});
// 'bbold/b'
// With a custom pattern
const params = captureUtmParameters(url, {
sanitize: {
enabled: true,
customPattern: /[!@#$%^&*]/g, // Strip additional characters
},
});Detect and filter personally identifiable information (email addresses, phone numbers) from UTM parameter values. Prevents PII from leaking into analytics via misconfigured tracking links. Disabled by default.
import { captureUtmParameters } from '@jackmisner/utm-toolkit';
// Reject mode (default) — discard values containing PII
const params = captureUtmParameters('https://example.com?utm_source=john@example.com&utm_medium=cpc', {
piiFiltering: { enabled: true },
});
// { utm_medium: 'cpc' } — utm_source was rejected
// Redact mode — replace PII values with [REDACTED]
const params = captureUtmParameters('https://example.com?utm_source=john@example.com&utm_medium=cpc', {
piiFiltering: { enabled: true, mode: 'redact' },
});
// { utm_source: '[REDACTED]', utm_medium: 'cpc' }
// Strict allowlist — only accept values matching a pattern
const params = captureUtmParameters(url, {
piiFiltering: {
enabled: true,
allowlistPattern: /^[a-z0-9_-]+$/, // Only lowercase alphanumeric, hyphens, underscores
},
});
// Callback for logging PII detections
const params = captureUtmParameters(url, {
piiFiltering: {
enabled: true,
onPiiDetected: (param, value, patternName) => {
console.warn(`PII detected in ${param}: matched ${patternName}`);
},
},
});Built-in PII patterns detect: email addresses, international phone numbers, UK phone numbers, and US phone numbers.
Hook into UTM lifecycle events for logging, analytics, or custom behavior.
import {
captureUtmParameters,
storeUtmParameters,
getStoredUtmParameters,
clearStoredUtmParameters,
appendUtmParameters,
} from '@jackmisner/utm-toolkit';
// onCapture — fired after UTM params are captured from a URL
captureUtmParameters(url, {
onCapture: (params) => console.log('Captured:', params),
});
// onStore — fired after params are written to storage
storeUtmParameters(params, {
onStore: (params, meta) => console.log(`Stored (${meta.storageType}):`, params),
});
// onClear — fired when stored params are cleared
clearStoredUtmParameters({
onClear: () => analytics.track('utm_params_cleared'),
});
// onAppend — fired after UTM params are appended to a URL
appendUtmParameters(url, params, {
onAppend: (finalUrl, params) => console.log('Appended:', finalUrl),
});
// onExpire — fired when TTL-expired data is auto-cleaned
getStoredUtmParameters({
storageType: 'local',
onExpire: (storageKey) => console.log(`Expired: ${storageKey}`),
});All callbacks are wrapped in try-catch — a throwing callback will never break the main pipeline.
Track how users first discovered your site vs. their most recent visit.
import { storeWithAttribution, getAttributedParams } from '@jackmisner/utm-toolkit';
// Mode: 'last' (default) — stores to main key, same as storeUtmParameters
storeWithAttribution(params, {
attribution: { mode: 'last' },
storageKey: 'utm_parameters',
storageType: 'session',
keyFormat: 'snake_case',
});
// Mode: 'first' — write-once; only stores on the first visit
storeWithAttribution(params, {
attribution: { mode: 'first' },
storageKey: 'utm_parameters',
storageType: 'session',
keyFormat: 'snake_case',
});
// Mode: 'both' — stores first-touch (write-once) and last-touch (always updates)
storeWithAttribution(params, {
attribution: { mode: 'both', firstTouchSuffix: '_first', lastTouchSuffix: '_last' },
storageKey: 'utm_parameters',
storageType: 'session',
keyFormat: 'snake_case',
});
// Read attributed params
const first = getAttributedParams({ ...opts, touch: 'first' });
const last = getAttributedParams({ ...opts, touch: 'last' });const {
utmParameters, // Current params (based on attribution mode)
firstTouchParams, // First-touch params (null when mode is 'last')
lastTouchParams, // Last-touch params (null when mode is 'first')
} = useUtmTracking({
config: { attribution: { mode: 'both' } },
});Build UTM-tagged URLs from structured input with validation and warnings.
import { buildUtmUrl, validateUtmValues } from '@jackmisner/utm-toolkit';
const result = buildUtmUrl({
url: 'https://example.com',
source: 'google',
medium: 'cpc',
campaign: 'spring2025',
});
// result.valid === true
// result.url === 'https://example.com?utm_source=google&utm_medium=cpc&utm_campaign=spring2025'
// result.errors === []
// result.warnings === []
// With options
const result = buildUtmUrl(
{ url: 'example.com', source: 'Google', campaign: 'Spring' },
{ normalize: true, lowercaseValues: true },
);
// Normalizes URL, lowercases all values, no uppercase warnings
// Validation errors
const result = buildUtmUrl({ url: 'https://example.com', source: 'goo&gle' });
// result.valid === false
// result.errors === ['source contains unsafe characters (& = ? #)']
// Standalone validation
const { errors, warnings } = validateUtmValues({ source: 'Google', medium: 'cpc' });
// errors === [], warnings === ['source contains uppercase characters']Inject stored UTM params into HTML form fields. Works with vanilla JS or React.
import { populateFormFields, createUtmHiddenFields } from '@jackmisner/utm-toolkit';
// Strategy: 'name' — find <input name="utm_source"> etc. and set values
populateFormFields({ strategy: 'name' });
// Strategy: 'data-attribute' — find <input data-utm="source"> etc.
populateFormFields({ strategy: 'data-attribute' });
// Strategy: 'auto-create' (default) — create hidden inputs in matching forms
populateFormFields({ selector: '#signup-form' });
// createUtmHiddenFields is a shortcut for auto-create strategy
createUtmHiddenFields({ selector: 'form.track-utm' });import { UtmHiddenFields, useUtmFormData } from '@jackmisner/utm-toolkit/react';
// Component — renders hidden <input> elements inside your form
function ContactForm() {
return (
<form action="/submit">
<input name="email" type="email" />
<UtmHiddenFields prefix="tracking_" />
<button type="submit">Submit</button>
</form>
);
}
// Hook — returns UTM data as a flat Record for form libraries
function MyForm() {
const utmData = useUtmFormData();
// { utm_source: 'google', utm_medium: 'cpc', ... }
return (
<form>
{Object.entries(utmData).map(([key, value]) => (
<input key={key} type="hidden" name={key} value={value} />
))}
</form>
);
}Auto-append UTM params to links on a page. Useful for internal navigation tracking.
import { decorateLinks, observeAndDecorateLinks } from '@jackmisner/utm-toolkit';
// Decorate all internal links
decorateLinks();
// With options
decorateLinks({
selector: 'a.track', // Custom CSS selector
internalOnly: true, // Only same-host links (default)
includeHosts: ['partner.com'], // Additional hosts to decorate
excludeHosts: ['cdn.example.com'], // Hosts to skip
skipExisting: true, // Don't re-decorate (default)
extraParams: { utm_campaign: 'nav' }, // Additional static params
onAppend: (url, params) => console.log('Decorated:', url),
});
// For SPAs — watch for new links via MutationObserver
const cleanup = observeAndDecorateLinks({ internalOnly: false });
// Later: cleanup() to disconnect the observerimport { UtmLinkDecorator, useUtmLinkDecorator } from '@jackmisner/utm-toolkit/react';
// Component wrapper — decorates child links
function Navigation() {
return (
<UtmLinkDecorator internalOnly={true}>
<nav>
<a href="/products">Products</a>
<a href="/pricing">Pricing</a>
</nav>
</UtmLinkDecorator>
);
}
// Hook — returns a ref to scope decoration to a container
function MySection() {
const ref = useUtmLinkDecorator({ internalOnly: false });
return (
<div ref={ref}>
<a href="https://partner.com">Partner Link</a>
</div>
);
}By default, UTM parameters are stored in sessionStorage (cleared when the tab closes). For longer-lived storage, use localStorage with an optional TTL.
import { storeUtmParameters, getStoredUtmParameters, createConfig } from '@jackmisner/utm-toolkit';
// Ephemeral storage (default) — cleared when tab closes
storeUtmParameters(params);
// Persistent storage — survives browser restarts
storeUtmParameters(params, { storageType: 'local' });
// Persistent storage with 24-hour TTL — auto-expires
storeUtmParameters(params, { storageType: 'local', ttl: 86400000 });
// Expired data returns null and is auto-cleaned from storage
const stored = getStoredUtmParameters({ storageType: 'local' });
// Use with React hook
const { utmParameters } = useUtmTracking({
config: {
storageType: 'local',
ttl: 86400000, // 24 hours
},
});import { createConfig } from '@jackmisner/utm-toolkit';
const config = createConfig({
enabled: true,
keyFormat: 'snake_case',
storageKey: 'utm_parameters',
storageType: 'session',
captureOnMount: true,
appendToShares: true,
allowedParameters: ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id'],
defaultParams: {},
shareContextParams: {
default: { utm_medium: 'social_share' },
linkedin: { utm_content: 'linkedin_share' },
copy: { utm_content: 'link_copy' },
},
excludeFromShares: ['utm_team_id'],
});import { useUtmTracking } from '@jackmisner/utm-toolkit/react';
function MyComponent() {
const {
utmParameters, // Current captured params (or null)
isEnabled, // Whether tracking is enabled
hasParams, // Whether any params exist
capture, // Manually capture from URL
clear, // Clear stored params
appendToUrl, // Append params to a URL
firstTouchParams, // First-touch params (null when attribution mode is 'last')
lastTouchParams, // Last-touch params (null when attribution mode is 'first')
} = useUtmTracking({
config: {
keyFormat: 'camelCase',
attribution: { mode: 'both' },
shareContextParams: {
linkedin: { utm_content: 'linkedin' },
},
onCapture: (params) => analytics.track('utm_captured', params),
onStore: (params, meta) => analytics.track('utm_stored', { ...params, touch: meta.touch }),
},
});
// Generate share URL with platform-specific params
const linkedInUrl = appendToUrl('https://example.com', 'linkedin');
}import {
debugUtmState,
checkUtmTracking,
installDebugHelpers,
} from '@jackmisner/utm-toolkit';
// Log current state to console
debugUtmState();
// Check for issues
const messages = checkUtmTracking();
messages.forEach(msg => console.log(msg));
// Install browser console helpers (add ?debug_utm=true to URL)
installDebugHelpers();
// Then use: window.utmDebug.state(), window.utmDebug.check()| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
true |
Enable/disable UTM tracking |
keyFormat |
'snake_case' | 'camelCase' |
'snake_case' |
Key format for returned params |
storageKey |
string |
'utm_parameters' |
Browser storage key |
storageType |
'session' | 'local' |
'session' |
Storage backend (sessionStorage or localStorage) |
ttl |
number |
undefined |
Time-to-live in ms for stored params (localStorage only) |
captureOnMount |
boolean |
true |
Auto-capture on React hook mount |
appendToShares |
boolean |
true |
Append UTM params to share URLs |
allowedParameters |
string[] |
Standard UTM params | Params to capture |
defaultParams |
object |
{} |
Fallback params when none captured |
shareContextParams |
object |
{} |
Platform-specific params |
excludeFromShares |
string[] |
[] |
Params to exclude from shares |
sanitize |
SanitizeConfig |
{ enabled: false } |
Value sanitization settings |
piiFiltering |
PiiFilterConfig |
{ enabled: false } |
PII detection and filtering |
attribution |
AttributionConfig |
{ mode: 'last' } |
First-touch/last-touch attribution |
onCapture |
function |
undefined |
Callback after UTM params are captured |
onStore |
function |
undefined |
Callback after UTM params are stored |
onClear |
function |
undefined |
Callback when stored params are cleared |
onAppend |
function |
undefined |
Callback after UTM params are appended to a URL |
onExpire |
function |
undefined |
Callback when stored params expire (TTL) |
import type {
UtmParameters,
UtmConfig,
StorageType,
KeyFormat,
SanitizeConfig,
PiiFilterConfig,
PiiPattern,
SharePlatform,
AttributionMode,
AttributionConfig,
TouchType,
ValidationResult,
UseUtmTrackingReturn,
} from '@jackmisner/utm-toolkit';- All modern browsers (Chrome, Firefox, Safari, Edge)
- Requires
sessionStorageorlocalStoragesupport - SSR-safe (returns empty/null values on server)
If you're migrating from a custom UTM implementation:
- Install the package
- Replace custom capture/storage/append functions with the library equivalents
- Update storage key if needed via
storageKeyoption - Test that existing UTM tracking still works
MIT