@observejs/browser
v0.1.0
Published
Observe.js browser SDK — framework-agnostic analytics, replay, and error tracking foundation.
Maintainers
Readme
@observejs/browser
Framework-agnostic browser SDK for Observe.js. Provides identity, event queuing, transport, and plugin infrastructure for all Observe modules (auto-track, replay, heatmaps, errors, network, performance, AI).
- ~10KB gzipped, zero runtime dependencies
- Tree-shakeable ES modules
- Works in browsers, SSR-safe (no-ops on the server)
- Plugin-based — add features without touching the core
- TypeScript-first with full type inference
Installation
npm install @observejs/browser
# or
pnpm add @observejs/browser
# or
yarn add @observejs/browserCDN:
<script type="module">
import { Observe } from "https://cdn.jsdelivr.net/npm/@observejs/browser/+esm";
Observe.init({ apiKey: "obs_pk_live_xxx" });
</script>Quick start
import { Observe } from "@observejs/browser";
Observe.init({
apiKey: "obs_pk_live_xxx",
environment: "production",
});
Observe.page();
Observe.identify("user_123", { email: "[email protected]", plan: "pro" });
Observe.track("Signup Completed", { source: "landing" });That's it. The SDK auto-batches events, retries on failure, persists offline, and flushes on page unload.
Configuration
All options for Observe.init(config):
| Option | Type | Default | Description |
| ----------------- | ----------------------------- | -------------------------------- | ----------- |
| apiKey | string required | — | Project tracking key (publishable). |
| apiUrl | string | https://ingest.observejs.com | Ingestion base URL. Override for self-hosting. |
| environment | string | "production" | Environment slug ("production", "staging", "dev", …). |
| release | string | — | App version / git SHA for source-map correlation. |
| debug | boolean | false | Verbose console logging. |
| batchSize | number | 20 | Events queued before an auto-flush. |
| flushInterval | number (ms) | 5000 | Periodic auto-flush interval. Min 500. |
| offlineQueue | boolean | true | Persist queue to localStorage while offline. |
| sampling | number 0..1 | 1 | Drop events outside the sample rate. |
| sessionTimeout | number (ms) | 1_800_000 (30 min) | Inactivity before a new session is started. |
| ignoreUrls | (string \| RegExp)[] | [] | Drop events whose url matches. |
| ignoreSelectors | string[] | [] | Selectors auto-track plugins should skip. |
| globalContext | Record<string, unknown> | {} | Static properties merged into every event's metadata. |
| plugins | Plugin[] | [] | Plugins registered at init. |
| enabled | boolean | true | Master kill-switch (use for opt-out flows). |
| fetchImpl | typeof fetch | globalThis.fetch | Override the fetch implementation (SSR / tests). |
Full configuration example
import { Observe } from "@observejs/browser";
Observe.init({
apiKey: import.meta.env.VITE_OBSERVE_KEY,
apiUrl: "https://ingest.observejs.com",
environment: import.meta.env.MODE, // "production" | "development"
release: __APP_VERSION__, // injected at build time
debug: import.meta.env.DEV,
batchSize: 25,
flushInterval: 3000,
offlineQueue: true,
sampling: 1,
sessionTimeout: 30 * 60 * 1000,
ignoreUrls: ["/healthz", /\/internal\//],
ignoreSelectors: ["[data-no-track]", ".sensitive"],
globalContext: {
appName: "acme-web",
tenant: "acme",
},
});Public API
Observe.init(config)
Boot the SDK. Idempotent — a second call logs a warning and is ignored. Throws are caught internally and never reach the host app.
Observe.init({
apiKey: "obs_pk_live_xxx",
environment: "production",
release: "1.4.2",
debug: false,
});SSR note:
init()is safe to call during render; the SDK detects the missingwindowand no-ops. Call it once at app bootstrap (e.g. in your root component'suseEffect, or before mounting).
Observe.track(eventName, properties?)
Record a custom event. Use for product analytics: button clicks, conversions, feature usage, custom milestones.
Observe.track("Signup Completed", {
source: "landing-page",
plan: "pro",
referrer: document.referrer,
});
Observe.track("Cart Item Added", {
productId: "prod_123",
price: 49.0,
currency: "USD",
quantity: 1,
});
Observe.track("Video Played", {
videoId: "intro",
position: 0,
duration: 142,
});Naming convention: use "Object Action" in title case ("Invoice Sent",
"Project Archived"). Properties should be JSON-serializable scalars or
arrays — avoid Dates, Maps, Sets, or circular structures.
Observe.page(properties?)
Record a pageview. Auto-captures url, path, referrer, and utm_* query
params (sticky for the session). Call once per route change.
// Vanilla
Observe.page();
// With overrides
Observe.page({
title: document.title,
category: "Docs",
});React Router / TanStack Router example:
import { useEffect } from "react";
import { useRouterState } from "@tanstack/react-router";
import { Observe } from "@observejs/browser";
function usePageviews() {
const { location } = useRouterState();
useEffect(() => {
Observe.page({ path: location.pathname });
}, [location.pathname]);
}Next.js App Router:
"use client";
import { useEffect } from "react";
import { usePathname, useSearchParams } from "next/navigation";
import { Observe } from "@observejs/browser";
export function ObservePageviews() {
const pathname = usePathname();
const params = useSearchParams();
useEffect(() => {
Observe.page();
}, [pathname, params]);
return null;
}Observe.identify(userId, traits?)
Bind the current anonymous visitor to a known user. Traits are persisted to
localStorage and re-attached to every subsequent event.
// On login / session restore
Observe.identify(user.id, {
email: user.email,
name: user.fullName,
plan: user.subscription.plan,
createdAt: user.createdAt,
});Related methods:
// Merge an anonymous visitor into a user (e.g. after signup)
Observe.alias("user_123" /*, optional previousVisitorId */);
// Associate the user with an account/organization
Observe.group("acme_corp", { name: "Acme Corp", plan: "enterprise" });
// Update traits without re-identifying
Observe.setUserProperties({ plan: "enterprise", trialEndsAt: "2026-08-01" });
// Clear identity on logout (rotates visitorId, starts a new session)
Observe.reset();Observe.flush()
Force the queue to flush immediately. Returns a Promise that resolves once
the in-flight batch settles. Useful before navigation, logout, or critical
business events.
// Before redirecting away from the app
async function checkout() {
Observe.track("Checkout Started", { cartTotal: 129.0 });
await Observe.flush();
window.location.href = "https://billing.stripe.com/...";
}
// Before logout, so the final events ship under the right user
async function logout() {
Observe.track("Signed Out");
await Observe.flush();
Observe.reset();
await supabase.auth.signOut();
}You usually don't need to call
flush()— the SDK already flushes onvisibilitychange→hiddenandpagehidevia the Beacon API.
Other methods
// Static context merged into every event's `metadata`
Observe.setGlobalContext({ tenant: "acme", featureFlags: { newNav: true } });
// Capture an error (typed: Error | string | unknown)
try {
riskyOperation();
} catch (err) {
Observe.captureException(err, { component: "Checkout" });
}
// Log a message with severity
Observe.captureMessage("Payment retried", "warn", { attempt: 2 });
// Tear everything down (flushes, removes listeners, clears plugins)
await Observe.shutdown();Event schema
Every event the SDK ships contains:
{
eventId: "uuid",
eventName: "Signup Completed",
type: "track",
timestamp: "2026-06-27T12:34:56.789Z",
sentAt: 1750000000000,
sessionId, visitorId, deviceId, userId,
projectKey, environment, release,
url, path, referrer,
utm: { source, medium, campaign, term, content },
device: {
browser, browserVersion,
os, osVersion,
device: "desktop" | "mobile" | "tablet" | "bot" | "unknown",
screen: { width, height, dpr },
language, timezone, userAgent,
},
level: "debug" | "info" | "warn" | "error" | "fatal",
metadata: { ...globalContext },
properties: { ...yourProps },
sdk: { name: "@observejs/browser", version: "0.1.0", api: "v1" },
}Plugins
The SDK is plugin-driven. Future packages (auto-track, replay, heatmaps, errors, network, AI) all plug into the same contract — no core changes needed.
import { Observe, definePlugin } from "@observejs/browser";
const consoleSpy = definePlugin({
name: "console-spy",
version: "1.0.0",
setup(ctx) {
const off = ctx.on("event", (e) => {
ctx.logger.debug("event:", e.eventName, e.properties);
});
return () => off(); // optional teardown
},
});
Observe.init({
apiKey: "obs_pk_live_xxx",
plugins: [consoleSpy],
});
// Or register after init
Observe.use(consoleSpy);Available hooks: init, event, beforeSend, page, identify, flush,
shutdown. Plugins receive a PluginContext with the resolved config,
identity getter, enqueue(), a logger, and the on() subscription helper.
A throwing plugin is isolated — it cannot crash the host app or the SDK.
Architecture
src/
├── core/ init, lifecycle, config, Observe singleton
├── identity/ visitor / device / session IDs, user binding
├── queue/ in-memory + offline (localStorage) batch queue
├── transport/ fetch + sendBeacon with retry/backoff
├── plugins/ plugin contract + registry
├── utils/ uuid, time, ua parsing, device, url, utm, emitter, logger
└── types.ts public TypeScript surfaceReliability
- Batching: flushes when
batchSizeis reached, everyflushInterval, onvisibilitychange→hidden, and onpagehide. - Retries: failed requests retry with exponential backoff (500ms → 30s, max 5 attempts) for transient errors (network, 408, 429, 5xx). Permanent 4xx responses drop the batch to avoid poison-pill loops.
- Offline: queue is mirrored to
localStorage(capped at 500 events) and restored on next page load. - Beacon: final flush during unload uses
navigator.sendBeaconso events ship even after the page is dismissed. - Safety: every public method is wrapped — internal errors are logged
(only when
debug: true) and never thrown into the host app.
TypeScript
All types are exported from the package root:
import type {
ObserveConfig,
ObserveEvent,
EventLevel,
DeviceInfo,
UtmParams,
Identity,
Plugin,
PluginContext,
PluginHook,
} from "@observejs/browser";License
MIT
