Facts reviewed: 2026-10-20


Initialize Owleye Analytics once at the application root. It records page views and SPA navigation automatically; call `track()` for confirmed product outcomes and `stop()` during teardown. To add supported interactions later from the dashboard, install the separate [rules entrypoint](/docs/rules/) once and choose stable selectors.

## Install

Install the published browser package:


### npm

```sh
npm install @owleye/analytics@1.0.3
```


### pnpm

```sh
pnpm add @owleye/analytics@1.0.3
```


### Bun

```sh
bun add @owleye/analytics@1.0.3
```




Using a script tag instead of a bundler? Follow [CDN & plain HTML](/docs/cdn/) or [WordPress](/docs/wordpress/).

## Start in about 30 seconds

Run this once in a browser entrypoint. In an SSR framework, put it in client-only code.

```ts
import { useAnalytics } from "@owleye/analytics";

const analytics = useAnalytics("your-site-id");

analytics.track("signup_clicked", {
  annual: true,
  plan: "starter",
  seats: 3,
});
```

`useAnalytics` immediately starts tracking and returns an `AnalyticsController`. It records the initial page view, SPA route changes, and the time spent on each page segment. Call `analytics.stop()` during teardown or withdrawal; it disables future tracking, discards the active page segment, and removes its browser listeners. Already-started requests cannot be retracted. `stop()` and `start()` are safe to call more than once.

```ts
// For a component or app lifecycle cleanup function:
return () => analytics.stop();
```

Repeated initialization for the same tracking ID and API base shares one active controller, even across npm and CDN copies. It does not install extra listeners or count extra pageviews. Cleanup/remount on the same URL also avoids another automatic pageview; actual navigation and explicit `pageview()` calls still count. The first configuration wins until the tracker is stopped. Repeated initialization never starts a controller created with `autoStart: false`; call `start()` explicitly. Conflicting options produce a bounded warning. To change GPC or other options, call `stop()` and create a new controller with the complete desired configuration. Own this lifecycle at the app root, since `stop()` affects all users of that shared controller.

Rule tracking and Web Vitals observation also reuse active instances. Distinct rule IDs and explicit `track()` calls remain separate events. Calling `track()` repeatedly in a render function still records repeated events; use actual interactions or a deliberate lifecycle hook instead.

Delivery is best effort: the document shares limits of eight in-flight event requests, 60,000 in-flight bytes, and 100 events/second with a burst allowance of 200. Event requests time out after five seconds; excess events are dropped without retries or a persistent queue. Rule loading times out after ten seconds. SDK failures are caught at public calls and background callbacks. Invalid initialization returns a safe no-op controller; invalid events are logged and dropped. Warnings identify the failed operation without payloads or raw exception messages and are limited to ten per minute per document. Even broken console methods cannot propagate logging errors. DOM matching and enrichment use the main thread; keep callbacks fast and selectors narrow.

## Track a custom event

Use stable, descriptive names such as `signup_completed` or `report_exported`. Pass one flat object in normal use:

```ts
analytics.track("report_exported", {
  format: "csv",
  rows: 842,
  scheduled: false,
});
```

- An event accepts at most ten custom fields in total.
- Event names are trimmed and must be 1–128 UTF-8 bytes with no control characters.
- Values can be strings, finite numbers, or booleans.
- Arrays, nested objects, `null`, `NaN`, and infinite numbers cause the event to be logged and dropped.
- The complete event data object is capped at 16 KiB before Owleye Analytics starts a request.
- Calling `track` while the controller is stopped is a no-op.

The flat payload is deliberate: fewer accidental identifiers, easier queries, less archaeological work for Future You.

## Shared event fields

`setGlobalRecords()` replaces the fields merged into future custom `track()` events. It does not add fields to page views, rules, or performance measurements. Call it without an argument to clear them.

```ts
analytics.setGlobalRecords({ app_version: "1.2", surface: "dashboard" });
analytics.track("report_exported", { format: "csv" });
analytics.setGlobalRecords();
```

Global and event fields share the ten-field limit; event values override matching global keys. `track()` also accepts multiple flat objects, up to ten objects and ten fields across them.

## Explicit page views

Normal page loads and SPA pathname changes are automatic. Query/hash-only changes require `trackQueryChanges` / `trackHashChanges`. Use `pageview()` only for a deliberate additional view, such as a virtual screen whose navigation does not change browser history. Each explicit call counts; do not call it again for an already tracked route change.

```ts
analytics.pageview({ path: "/onboarding/complete", title: "Setup complete" });
```

The optional argument is `Partial<OwlPage>`. Its string fields are `path`, `title`, `url`, `host`, `referrer`, `referrer_host`, `search`, and `hash`. Omitted fields come from the current browser page; a supplied `path` derives the URL when `url` is omitted. Explicit overrides are sent as supplied: remove sensitive query strings and fragments yourself, and keep related fields consistent when overriding a URL. Calling without an argument records the current page; calling while stopped does nothing.

## SPA and memory routers

Automatic pageviews count pathname changes by default. Query-only and hash-only changes do not create pageviews or close the current page segment. To count those route changes, enable the relevant option:

```ts
const analytics = useAnalytics("owl_your_tracking_id", {
  trackQueryChanges: true,
  trackHashChanges: true,
});
```

These options control counting, not captured URL content. `captureQuery` and `captureHash` remain separate opt-ins. Leave them off unless those values are safe to send. Conversely, enabling URL capture alone does not enable query/hash routing. Repeated browser notifications for the same URL count once.

Memory routers have no standard browser navigation event. Connect your router's committed-route callback to `pageview()` and disable automatic views so the browser URL is not also counted:

```ts
const analytics = useAnalytics("owl_your_tracking_id", {
  autoTrackPageviews: false,
});

// Call once for the initial screen and once after each completed navigation.
// Wire this function to your router's subscription or framework lifecycle.
function onRouteCommitted(path: string, title: string) {
  analytics.pageview({ path, title });
}
```

Use a sanitized virtual pathname without secrets. Supplying `path` also derives the page URL when no explicit `url` is given. In manual mode, page sessions and custom events use the last explicit page context. Owleye Analytics cannot infer memory-router transitions from DOM changes, and repeated explicit calls each count as a view. Optional rules and performance modules still use browser/document context, not the memory router's virtual route.

For CDN, use `data-owleye-track-query-changes="true"`, `data-owleye-track-hash-changes="true"`, or `data-owleye-auto-track-pageviews="false"`, and call `window.OwlEyeAnalytics.pageview({ path, title })` from the same route callback. Configure tracking permission separately; `autoTrackPageviews: false` does not disable custom events.

## Configuration

| Option | Default | What it does |
| --- | --- | --- |
| `server` | Owleye Analytics hosted service | Custom API origin (HTTP(S)) or same-origin API path such as `/analytics`. |
| `autoStart` | `true` | Only controls `useAnalytics()` startup; does not delay rules or performance collection. |
| `respectGlobalPrivacyControl` | `true` | Disables tracking when the browser exposes an enabled Global Privacy Control signal. |
| `captureQuery` | `false` | Includes the full URL query string. Use only after checking what your app puts there. |
| `captureHash` | `false` | Includes URL fragments. Enable only when the fragment is useful and safe. |
| `autoTrackPageviews` | `true` | Count initial and browser navigation views automatically. Disable for manual/memory-router views. |
| `trackQueryChanges` | `false` | Count query-only navigation independently of query capture. |
| `trackHashChanges` | `false` | Count hash-only navigation independently of fragment capture. |
| `debug` | `false` | Explain delivery decisions and numeric acknowledgements, without event payloads or raw response text. |
| `mock` | `false` | Sends no network requests. Pair it with `debug` to verify suppression locally. |

SDK 1.0.3 automatically captures bounded `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, and `utm_content` values from the current page. Arbitrary query parameters and URL fragments remain excluded by default. Use broad, non-personal campaign labels; never put emails, recipient IDs, secrets, or click IDs in campaign fields. No campaign settings or browser storage are required, and attribution is not persisted after the tags disappear from the URL. Full `captureQuery` deliberately exposes the broader query string, subject to the ingestion field-size limit. Remove legacy `captureCampaigns` options: the deprecated field is ignored, even when false. Upgrade the API before the SDK, and review your visitor notice for the new automatic attribution default.

Site IDs are trimmed and must contain 1–128 ASCII letters, numbers, dots, underscores, or hyphens. The SDK rejects invalid IDs and API bases during setup, before installing listeners or making a request.

Debug mode explains GPC/inactive suppression, mock mode, request-budget drops, HTTP/network failures, and zero-event acknowledgements. HTTP success alone does not prove acceptance or warehouse storage. Response inspection is bounded to 4 KiB and debug messages to 100 per minute per document. Non-debug delivery does not parse response bodies; bounded failure/configuration warnings still apply.

## TypeScript

Type declarations ship with the package for all three module entrypoints. No separate `@types` package is needed. JavaScript usage is identical; TypeScript adds inferred controller methods, configuration completion, and checks for supported primitive event values.

```ts
import {
  useAnalytics,
  type OwlConfig,
  type OwlRecord,
} from "@owleye/analytics";
import { trackRules, type OwlRulesConfig } from "@owleye/analytics/rules";
import { trackWebVitals } from "@owleye/analytics/performance";

const config = { debug: false } satisfies OwlConfig;
const fields = { plan: "business", seats: 3 } satisfies OwlRecord;
const analytics = useAnalytics("your-site-id", config);
analytics.track("signup_completed", fields);

const ruleConfig = {
  ...config,
  enrichRule(rule, context) {
    return { rule: rule.name, interaction: context.type };
  },
} satisfies OwlRulesConfig;
const rules = trackRules("your-site-id", ruleConfig);
const vitals = trackWebVitals("your-site-id", config);

// In your application's teardown hook:
function cleanup() {
  analytics.stop();
  rules.stop();
  vitals.stop();
}
```

Useful exported types:

| Import path | Types |
| --- | --- |
| `@owleye/analytics` | `OwlConfig`, `AnalyticsController`, `OwlPage`, `OwlRecord`, `OwlRecordValue`, `OwlTrackRecords`, `OwlPerfRecords`, `OwlEvent`, `OwlEventType`, `OwlEventEnvelope`, `OwlEnvironment` |
| `@owleye/analytics/rules` | `OwlConfig`, `OwlRulesConfig`, `RulesController`, `RuleEnricher`, `RuleEnrichmentContext`, `TrackingRule`, `RulesResponse`, `RuleCustomProperty`, `RuleDomEvent`, `RuleEventType`, `RuleKeyCondition`, `RuleMetadata`, `RuleSelectorType`, `RuleTriggerConfig` |
| `@owleye/analytics/performance` | `OwlConfig`, `OwlRecord`, `OwlRecordValue`, `OwlPerfRecords`, `PerfController`, `PerfEnd`, `WebVitalsController` |

Use `import type` for types and the normal imports for functions. The package uses ESM; use a TypeScript resolver that understands package exports, such as `bundler` or `NodeNext`, with DOM types available. The SDK still runs in the browser, so initialize it in client-only framework code. The `satisfies` syntax in this example requires TypeScript 4.9 or later.

Types do not enforce byte-size limits, finite numbers, privacy decisions, or the total number of object fields. Runtime validation still applies. Unknown options such as `performance: true` are not part of `OwlConfig`; use the performance module instead.

## Optional, tree-shakeable entrypoints

Page analytics, interaction rules, and performance measurements are separate entrypoints. Import only what the site uses; a bundler does not need to ship rule tracking just because you measure a checkout.

### Rule tracking

```ts
import { trackRules } from "@owleye/analytics/rules";

const rules = trackRules("your-site-id");

// During app teardown:
rules.stop();
```

The browser fetches enabled rules from `GET /v1/rules`. Click and submit rules use delegated event listeners; view rules use one `IntersectionObserver` plus a debounced child-list observer so targets inserted asynchronously are discovered without re-reporting handled elements. Page-scoped matches also rebuild after an SPA navigation. `rules.stop()` aborts an in-flight rule fetch, disconnects both observers, restores shared navigation hooks, clears pending timers, and removes the listeners.

An optional synchronous enricher can attach up to ten primitive fields. If it throws or returns invalid data, Owleye Analytics still sends the base rule event.

```ts
const rules = trackRules("your-site-id", {
  enrichRule(rule, context) {
    return {
      experiment: "pricing_v2",
      rule: rule.name,
      interaction: context.type,
    };
  },
});
```

`enrichRule` is the rules module's additional configuration option. It receives a readonly rule and a readonly context with the matched `element`, interaction `type`, and optional native `event` (absent for visibility and delayed debounce matches). Return a flat record or nothing; asynchronous callbacks are not supported. Avoid reading personal information from the element or event. Use the module import when providing a callback; script-tag attributes cannot carry functions.

Rule matches apply `sample_rate` independently and do not store a sampling ID in the browser. Authenticated rule management remains behind the site-scoped console API; the public SDK only reads the enabled set.

### Custom timing tracking

```ts
import { trackPerf } from "@owleye/analytics/performance";

const timings = trackPerf("your-site-id");
const endCheckout = timings.start("checkout_submit", {
  cart_items: 3,
  source: "checkout",
});

try {
  await submitCheckout();
  endCheckout({ status: "ok" });
} catch (error) {
  endCheckout({ status: "error" });
  throw error;
}
```

`start` returns an end function for that measurement. The end function is idempotent, so a second call is ignored. Start and end data accept at most ten primitive fields in total per call.

Each completed timing is a `performance` event carrying `duration_ms`. It is stored in the separate performance dataset and excluded from ordinary event lists and paid-plan monthly event usage. Ask the AI assistant for counts and duration summaries using the recorded timing name. The **Web Vitals** console page reports automatic browser measurements only, not these custom timings.

## Plain script build

See [CDN & plain HTML](/docs/cdn/) for version-pinned hosted script URLs and all data attributes. You can also copy `dist/owleye.analytics.iife.js` to your own static assets, then load it like this:

```html
<script
  defer
  src="/vendor/owleye.analytics.iife.js"
  data-owleye-id="your-site-id"
></script>
```

Equivalent rule and performance builds are emitted as `owleye.rules.iife.js` and `owleye.performance.iife.js`. Use `owleye.full.iife.js` for all three modules in one payload with the same globals and script attributes; see [Full CDN bundle](/docs/cdn/#full-cdn-bundle) for setup, release availability, and lifecycle details. Prefer the ESM entrypoints for application code because they give bundlers the clearest tree-shaking boundary.

## Browser privacy defaults

The public SDK does not write cookies, local storage, or session storage. Event requests omit browser credentials and send no referrer header. These claims apply to public visitor tracking; the authenticated Owleye Analytics console can use secure, HttpOnly session cookies.

The SDK stops tracking for Global Privacy Control by default. It checks `navigator.globalPrivacyControl` when the controller is initialized; the browser owns the `Sec-GPC` header, and installing Owleye Analytics does not enable that browser preference. Integrations can explicitly set `respectGlobalPrivacyControl: false` (or `data-owleye-respect-global-privacy-control="false"` for CDN scripts). Apply the setting separately to each optional entrypoint. The override does not remove the browser header. Do Not Track is ignored and has no SDK configuration option. The ingestion API does not filter DNT or GPC headers; integrations must apply collection choices before sending events. Cookie-free tracking can still require a lawful basis, notice, or consent depending on your use and jurisdiction. To wait for your consent manager or another documented decision, create an inert controller and start it later:

```ts
const analytics = useAnalytics("your-site-id", {
  autoStart: false,
});

analytics.start(); // only after the applicable decision
analytics.stop(); // on withdrawal or when processing must stop
```

See [privacy controls and data handling](/docs/privacy-compliance/) for retention, pseudonymous identifiers, rights handling, and operator responsibilities.

Error tracking is not part of the current public SDK surface.

## Core and optional performance collection

Core page analytics does not include the performance observer. The former `performance: true` core option has been removed. If you used it, initialize the optional entrypoint explicitly after the same consent or lawful-basis decision:

```ts
import { trackWebVitals } from "@owleye/analytics/performance";

const vitals = trackWebVitals("your-site-id");
// During teardown, discard pending measurements and remove observers.
vitals.stop();
```

The collector reports PerformanceObserver estimates with `approximate: true`. It does not implement the full reference CLS session-window or INP percentile algorithms. Do not compare these estimates as if they were identical to CrUX or the reference web-vitals library.

Global properties set with `analytics.setGlobalRecords()` and per-event properties share one ten-field limit. Per-event values override the same global key. A combined payload with more than ten distinct fields is logged and dropped locally without throwing into the application.

## Secret ingestion and public identifiers

Browser tracking uses the public site ID and an allowed origin. The legacy `/track` endpoint now requires the same real, site-scoped `events:write` API key as `/v1/events/api`, supplied as `Authorization: Bearer ...` or `x-owleye-api-key`. A public ID in `x-owleye-site-key` no longer authenticates a request. Never embed a secret API key in client-side code.