npm install @tell-rs/browser
A client-side singleton that handles device IDs, sessions, and delivery automatically. No user ID needed on track calls.
No cookies. No IP addresses. No cross-site tracking. Visitors are identified by a random device UUID stored in localStorage. The SDK collects anonymous device metadata only — you choose what properties to send. See Audit & Privacy for the full privacy model.
Quick start
import { tell } from "@tell-rs/browser";
tell.configure("a1b2c3d4e5f60718293a4b5c6d7e8f90");
tell.track("Page Viewed", { url: "/home" });
tell.identify("user_123", { name: "Jane", plan: "pro" });
Events called before configure() are queued and replayed once ready.
Verify it works
Open your browser’s DevTools Network tab and look for a POST request to https://collect.tell.app/v1/events. If you see a 202 response, the SDK is sending data successfully. In development mode, the SDK also logs to the console at debug level.
Script tag (no bundler)
If you don’t use a bundler, load the SDK directly from a CDN. The global build exposes a Tell object on window.
<script src="https://cdn.jsdelivr.net/npm/@tell-rs/browser@latest/dist/tell.global.js"></script>
<script>
var tell = Tell.tell;
tell.configure("YOUR_API_KEY");
tell.track("Page Viewed", { url: location.pathname });
</script>
All configuration options, tracking methods, and privacy controls work the same as the npm install — only the loading mechanism differs.
SPA page view tracking
For single-page apps without a framework router, hook into the History API to track navigations:
<script src="https://cdn.jsdelivr.net/npm/@tell-rs/browser@latest/dist/tell.global.js"></script>
<script>
var tell = Tell.tell;
tell.configure("YOUR_API_KEY");
function trackPageView() {
tell.track("Page Viewed", {
url: location.href,
path: location.pathname,
referrer: document.referrer,
title: document.title,
});
}
trackPageView();
var origPush = history.pushState;
var origReplace = history.replaceState;
history.pushState = function () {
origPush.apply(this, arguments);
trackPageView();
};
history.replaceState = function () {
origReplace.apply(this, arguments);
trackPageView();
};
window.addEventListener("popstate", trackPageView);
</script>
Pin a version
Replace @latest with a specific version to avoid unexpected changes in production:
https://cdn.jsdelivr.net/npm/@tell-rs/browser@0.3.2/dist/tell.global.js
Configuration
tell.configure("YOUR_API_KEY", {
endpoint: "https://collect.tell.app",
batchSize: 20,
flushInterval: 5_000,
sessionTimeout: 30 * 60 * 1000,
maxSessionLength: 24 * 60 * 60 * 1000,
persistence: "localStorage",
respectDoNotTrack: false,
botDetection: true,
captureErrors: false,
onError: (e) => console.error("[Tell]", e),
});
For local development, point the SDK at your Tell server:
tell.configure("YOUR_API_KEY", {
endpoint: "http://localhost:8080",
batchSize: 5,
flushInterval: 2_000,
logLevel: "debug",
});
Tracking events
No user ID parameter — the SDK tracks the current user automatically (anonymous until you call identify). For a full explanation of the identity model (device ID, anonymous ID, user ID), see Users & Identity.
tell.track("Feature Used", { feature: "dark_mode", enabled: true });
tell.identify("user_123", { name: "Jane", email: "jane@example.com" });
tell.group("company_456", { plan: "enterprise" });
tell.revenue(49.99, "USD", "order_789", { product: "annual_plan" });
tell.alias("anon_visitor_abc", "user_123");
For details on each event type and when to use them, see Events & Properties.
Standard event names
import { tell, Events } from "@tell-rs/browser";
tell.track(Events.PageViewed, { url: "/pricing" });
tell.track(Events.UserSignedUp, { source: "organic" });
tell.track(Events.FeatureUsed, { feature: "export" });
Constants cover user lifecycle, revenue, subscriptions, trials, shopping, engagement, and communication events. Custom string names always work too.
Super properties
Properties automatically attached to every track, group, and revenue call:
tell.register({ app_version: "2.1.0" });
// Every track call now includes app_version
tell.track("Click", { button: "submit" });
tell.unregister("app_version");
Event-specific properties override super properties when keys conflict. See Events & Properties for more on property patterns.
Structured logging
tell.logError("Payment failed", { error: "card_declined", amount: 9.99 });
tell.logInfo("User signed in", { method: "oauth" });
tell.logWarning("High memory usage");
Nine severity levels from logEmergency to logTrace. See Logs for the full level reference and when to use each.
Sessions
Sessions are managed automatically. The session ID is persisted to localStorage, so page navigations within the same site continue the same session. A new session starts after 30 minutes of inactivity (configurable via sessionTimeout) or after 24 hours (configurable via maxSessionLength). The SDK also captures UTM parameters from the URL as super properties.
For the full session lifecycle model, see Sessions.
Data collected automatically
The browser SDK captures device context on every session. No PII is collected — all fields are anonymous device and browser metadata.
| Data | Example value | Field |
|---|
| Full page URL (incl. query strings) | https://example.com/pricing?ref=blog | context.url |
| Page title | Pricing - Acme | context.title |
| Referrer URL | https://google.com/search?q=acme | context.referrer |
| Referrer domain | google.com | context.referrer_domain |
| UTM parameters | utm_source=twitter | Super properties |
| Browser + version | Chrome 120 | context.browser, context.browser_version |
| OS + version | macOS 14.2 | context.os_name, context.os_version |
| Device type | desktop | context.device_type |
| Screen dimensions | 2560 x 1440 | context.screen_width, context.screen_height |
| Viewport dimensions | 1280 x 720 | context.viewport_width, context.viewport_height |
| Device pixel ratio | 2 | context.device_pixel_ratio |
| CPU cores | 8 | context.cpu_cores |
| Device memory | 16 (GB) | context.device_memory |
| Locale | en-US | context.locale |
| Timezone | America/New_York | context.timezone |
| Connection type | 4g | context.connection_type |
| Touch support | false | context.touch |
URLs include query strings. If your app puts tokens or sensitive data in URLs (e.g. ?token=abc), use beforeSend or the redact() utility below to strip them before events leave the browser.
Redaction & beforeSend
The beforeSend hook lets you transform or drop events before they’re queued. Return the modified event, or null to drop it entirely.
Drop events from routes
tell.configure("YOUR_API_KEY", {
beforeSend: (event) => {
const url = event.context?.url;
if (typeof url === "string" && new URL(url).pathname.startsWith("/internal")) {
return null; // drop internal pages
}
return event;
},
});
Strip query params from URLs
tell.configure("YOUR_API_KEY", {
beforeSend: (event) => {
if (event.context?.url && typeof event.context.url === "string") {
const u = new URL(event.context.url);
u.searchParams.delete("token");
u.searchParams.delete("api_key");
return { ...event, context: { ...event.context, url: u.toString() } };
}
return event;
},
});
Remove PII from properties
tell.configure("YOUR_API_KEY", {
beforeSend: (event) => {
if (event.traits?.email) {
return { ...event, traits: { ...event.traits, email: "[REDACTED]" } };
}
return event;
},
});
Scrub log data
tell.configure("YOUR_API_KEY", {
beforeSendLog: (log) => {
if (log.data?.password) {
return { ...log, data: { ...log.data, password: "[REDACTED]" } };
}
return log;
},
});
User opt-out via beforeSend
tell.configure("YOUR_API_KEY", {
beforeSend: (event) => {
if (localStorage.getItem("analytics-optout")) return null;
return event;
},
});
redact() utility
For common redaction patterns, use the built-in redact() factory instead of writing beforeSend by hand:
import { tell, redact, redactLog, SENSITIVE_PARAMS } from "@tell-rs/browser";
tell.configure("YOUR_API_KEY", {
beforeSend: redact({
// Drop events from internal routes
dropRoutes: ["/internal", "/health"],
// Strip sensitive query params from all URLs
stripParams: [...SENSITIVE_PARAMS, "session_id"],
// Replace matching property/trait keys with "[REDACTED]"
redactKeys: ["email", "phone", "ssn"],
}),
beforeSendLog: redactLog({
redactKeys: ["password", "credit_card"],
}),
});
redact() and redactLog() return standard beforeSend functions. You can combine them with your own in an array:
tell.configure("YOUR_API_KEY", {
beforeSend: [
redact({ stripParams: SENSITIVE_PARAMS }),
myCustomHook,
],
});
Client-side redaction is your first line of defense. For defense in depth, also configure server-side pipeline redaction to catch anything that slips through.
Error auto-capture
Set captureErrors: true to automatically log uncaught errors and unhandled promise rejections.
tell.configure("YOUR_API_KEY", { captureErrors: true });
What it captures
window.onerror — uncaught exceptions:
message — the error message
filename — source file URL
lineno / colno — line and column number
stack — stack trace (when available)
unhandledrejection — unhandled promise rejections:
message — the rejection reason (or "Unhandled promise rejection")
stack — stack trace (when the reason is an Error)
Both are logged via logError with service "browser" and appear in your Tell logs dashboard.
When to use vs. your own error boundary
captureErrors is a catch-all safety net for errors that escape your application code. If you use React error boundaries or framework error handlers, you may prefer to log to Tell explicitly from those:
tell.logError(error.message, { stack: error.stack, component: "Checkout" });
Privacy
tell.optOut(); // Stop all tracking — events are dropped locally
tell.optIn(); // Resume tracking
tell.isOptedOut(); // Check current state
tell.reset(); // Clear user, device ID, session, super properties
Opt-out state is persisted to localStorage. Set respectDoNotTrack: true to honor the browser’s Do Not Track setting.
Lifecycle
tell.enable(); // Re-enable after disable()
tell.disable(); // Pause tracking
tell.reset(); // Clear user, device, session (e.g. on logout)
await tell.flush(); // Flush pending events
await tell.close(); // Flush + shut down
Error handling
Tracking calls (track, identify, group, revenue, alias) never throw. Validation errors and network failures route through the onError callback:
tell.configure("YOUR_API_KEY", {
onError: (err) => console.error("[Tell]", err.message),
});
Retry behavior
On HTTP send failure (5xx or network error), the SDK retries with exponential backoff: 1-second base delay, 1.5x multiplier, 20% jitter, capped at 30 seconds. After maxRetries attempts (default: 5), the batch is dropped and reported via onError.
The SDK also checks navigator.onLine before each attempt and skips if the browser reports offline.
Browser-specific features
Bot detection — automatically disables tracking when navigator.webdriver is set or the user agent contains “headless”. Controlled by botDetection (default: true).
Page unload — the SDK flushes via navigator.sendBeacon on beforeunload and visibilitychange to minimize data loss.
Pre-init queue — events called before configure() are buffered (up to 1,000) and replayed once the SDK is ready.
Configuration reference
| Parameter | Default | Description |
|---|
endpoint | https://collect.tell.app | HTTP collector URL |
batchSize | 20 | Events per batch before auto-flush |
flushInterval | 5,000 ms | Time between auto-flushes |
maxRetries | 5 | Retry attempts on send failure |
closeTimeout | 5,000 ms | Max wait on close() |
networkTimeout | 10,000 ms | HTTP request timeout |
sessionTimeout | 1,800,000 ms | Inactivity time before new session |
maxSessionLength | 86,400,000 ms | Max session duration (prevents zombie sessions) |
persistence | localStorage | "localStorage" or "memory" |
respectDoNotTrack | false | Honor browser DNT setting |
botDetection | true | Auto-disable for bots/headless |
captureErrors | false | Auto-log uncaught errors |
logLevel | error | SDK log verbosity |
maxQueueSize | 1,000 | Max queued items before oldest drops |