Initialize OwlEye 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 once and choose stable selectors.
Install
Section titled “Install”Install the published browser package:
npm install @owleye/analytics@1.0.3pnpm add @owleye/analytics@1.0.3bun add @owleye/analytics@1.0.3Using a script tag instead of a bundler? Follow CDN & plain HTML or WordPress.
Start in about 30 seconds
Section titled “Start in about 30 seconds”Run this once in a browser entrypoint. In an SSR framework, put it in client-only code.
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.
// 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
Section titled “Track a custom event”Use stable, descriptive names such as signup_completed or report_exported. Pass one flat object in normal use:
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 starts a request.
- Calling
trackwhile 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
Section titled “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.
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
Section titled “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.
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
Section titled “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:
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:
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 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
Section titled “Configuration”| Option | Default | What it does |
|---|---|---|
server |
OwlEye 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
Section titled “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.
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
Section titled “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
Section titled “Rule tracking”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 still sends the base rule event.
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
Section titled “Custom timing tracking”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
Section titled “Plain script build”See CDN & plain HTML 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:
<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 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
Section titled “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 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 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:
const analytics = useAnalytics("your-site-id", { autoStart: false,});
analytics.start(); // only after the applicable decisionanalytics.stop(); // on withdrawal or when processing must stopSee privacy controls and data handling 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
Section titled “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:
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
Section titled “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.