Skip to main content
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.
DataExample valueField
Full page URL (incl. query strings)https://example.com/pricing?ref=blogcontext.url
Page titlePricing - Acmecontext.title
Referrer URLhttps://google.com/search?q=acmecontext.referrer
Referrer domaingoogle.comcontext.referrer_domain
UTM parametersutm_source=twitterSuper properties
Browser + versionChrome 120context.browser, context.browser_version
OS + versionmacOS 14.2context.os_name, context.os_version
Device typedesktopcontext.device_type
Screen dimensions2560 x 1440context.screen_width, context.screen_height
Viewport dimensions1280 x 720context.viewport_width, context.viewport_height
Device pixel ratio2context.device_pixel_ratio
CPU cores8context.cpu_cores
Device memory16 (GB)context.device_memory
Localeen-UScontext.locale
TimezoneAmerica/New_Yorkcontext.timezone
Connection type4gcontext.connection_type
Touch supportfalsecontext.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

ParameterDefaultDescription
endpointhttps://collect.tell.appHTTP collector URL
batchSize20Events per batch before auto-flush
flushInterval5,000 msTime between auto-flushes
maxRetries5Retry attempts on send failure
closeTimeout5,000 msMax wait on close()
networkTimeout10,000 msHTTP request timeout
sessionTimeout1,800,000 msInactivity time before new session
maxSessionLength86,400,000 msMax session duration (prevents zombie sessions)
persistencelocalStorage"localStorage" or "memory"
respectDoNotTrackfalseHonor browser DNT setting
botDetectiontrueAuto-disable for bots/headless
captureErrorsfalseAuto-log uncaught errors
logLevelerrorSDK log verbosity
maxQueueSize1,000Max queued items before oldest drops