Facts reviewed: 2026-10-21

Use the standalone browser bundles for plain HTML, CMS templates, and site builders. SDK `@owleye/analytics@1.0.3` includes three individual bundles and the full bundle described below. Pin the version, test upgrades, and update the URL deliberately.

SDK 1.0.3’s standalone analytics script is approximately **7.8 KB gzip** (gzip level 9; transfer size varies by CDN compression). Rules and performance are optional downloads. CI checks bundle-size budgets; this is not a sub-3 KB tracker.

## Page views in one script

Copy your app's Tracking ID from the console. Add this once to your site's shared `<head>` or footer template:

```html
<script
  defer
  src="https://cdn.jsdelivr.net/npm/@owleye/analytics@1.0.3/dist/owleye.analytics.iife.js"
  data-owleye-id="owl_your_tracking_id"
></script>
```

The script starts page analytics automatically, including supported SPA navigation. Do not also initialize a package-manager installation for the same page. The hosted endpoint is already configured.

Alternatively, serve the same versioned file from your own `/vendor/` directory. This changes where the JavaScript is downloaded; events still go to Owleye Analytics. For stricter supply-chain controls, the package includes `dist/integrity.json` with SHA-384 hashes. Use the hash for the exact downloaded file with `integrity` and `crossorigin="anonymous"`.

## CDN campaigns, custom data, rules, and performance

The CDN globals are `window.OwlEyeAnalytics`, `window.OwlEyeRules`, and `window.OwlEyePerformance`. Each is available only after its corresponding bundle loads successfully. There is no single `window.OwlEye` object and no pre-load queue.

Load these scripts once with the same Tracking ID and endpoint. Keep `defer` (rather than `async`) when a later script depends on their order:

```html
<script
  defer
  src="https://cdn.jsdelivr.net/npm/@owleye/analytics@1.0.3/dist/owleye.analytics.iife.js"
  data-owleye-id="owl_your_tracking_id"
></script>
<!-- Optional: enabled rules configured in Console -->
<script
  defer
  src="https://cdn.jsdelivr.net/npm/@owleye/analytics@1.0.3/dist/owleye.rules.iife.js"
  data-owleye-id="owl_your_tracking_id"
></script>
<!-- Optional: automatic Web Vitals and manual timings -->
<script
  defer
  src="https://cdn.jsdelivr.net/npm/@owleye/analytics@1.0.3/dist/owleye.performance.iife.js"
  data-owleye-id="owl_your_tracking_id"
></script>
<script defer src="/analytics-events.js"></script>
```

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.

In your own **analytics-events.js**, use the initialized globals directly:

```js
const analytics = window.OwlEyeAnalytics;

// Optional fields reused by custom events. Never put personal identifiers here.
analytics?.setGlobalRecords({
  app_version: "1.0.0",
  environment: "production",
});

// A manual custom event: values retain their string / number / boolean types.
document.getElementById("download-report")?.addEventListener("click", () => {
  analytics?.track("report_downloaded", {
    format: "pdf",
    page_count: 12,
    included_charts: true,
  });
});

// Time your real asynchronous operation; no automatic page analytics needed.
async function loadReport() {
  const end = window.OwlEyePerformance?.start("report_load", { format: "pdf" });
  try {
    const response = await fetch("/reports/latest");
    end?.({ ok: response.ok, status_code: response.status });
    return response;
  } catch (error) {
    end?.({ ok: false, failure: "network" });
    throw error;
  }
}
```

For an inline `<script>`, wrap setup code in a `DOMContentLoaded` listener: inline scripts execute immediately even when preceded by deferred external scripts. For a dynamically injected CDN script, wait for its `load` event before using the global. Optional chaining prevents an exception when a script is blocked; it drops the call and does not retry it. If scripts can arrive later, read the global at call time instead of caching an initially undefined reference.

Rules need no manual JS handler: configure and enable a rule in Console, load the rules bundle, and trigger the matching interaction. Configure its custom-property selectors there too. Do not add a manual `.track()` for the same action unless you intend two events. A JavaScript `enrichRule` callback is a module-import option; it cannot be passed as a CDN HTML attribute. Use manual `.track()` for computed custom data in plain JavaScript.

The performance bundle starts Web Vitals automatically. Manual spans use `window.OwlEyePerformance.start()` as shown above. To stop Web Vitals, call `window.OwlEyePerformance?.observeVitals().stop()`; to stop rules, call `window.OwlEyeRules?.stop()`. The performance global itself has no `.stop()`. Only load optional bundles after any required tracking-permission decision; `data-owleye-auto-start="false"` delays the analytics bundle alone.

### Custom-property contract

- Custom events: **10 fields total**, combining global and event-specific fields; event-specific values override a matching global key.
- Rules: **10 fields total**, combining configured DOM captures and optional module-based enrichment. Configured captures take precedence.
- Performance: **10 fields at start and 10 at end**. Built-in timing fields are separate; the entire event data still must fit within **16 KiB**.
- Values: **string, finite number, or boolean** only. No `null`, `undefined`, arrays, nested objects, `Date` instances, bigint, functions, `NaN`, or infinities. Missing rule elements are omitted. Use an ISO string or numeric timestamp for a date; use named flat fields for structured concepts. JSON encoded as a string stays an opaque string, not queryable nested properties.

Invalid manual data is logged and dropped by the SDK without throwing into your application. The API also validates custom properties. Check the request to `/v1/events` and its `accepted` count; look for the event in Console's Events page, UTM attribution in Campaigns, and automatic measurements in Web Vitals.

## Full CDN bundle

Use `dist/owleye.full.iife.js` when you want page analytics, Console-managed rules, Web Vitals, and manual performance timers together. It is one self-contained JavaScript payload with shared code, not a loader that downloads three more scripts. It still makes the normal rules and event API requests. Keep the smaller individual bundles when you only need some features.

Load the full bundle from the version-pinned CDN URL:

```html
<script
  defer
  src="https://cdn.jsdelivr.net/npm/@owleye/analytics@1.0.3/dist/owleye.full.iife.js"
  data-owleye-id="owl_your_tracking_id"
></script>
```

The npm export is `@owleye/analytics/cdn/full`. The full bundle is included starting with SDK 1.0.1. The CDN URL becomes available after that version is published to npm. You can also copy the built file to your own static assets. Its SHA-384 value is included in `dist/integrity.json` alongside the individual bundles.

All documented script attributes apply to the full bundle. They are read from the script tag and passed to all included modules. Analytics-only options (`autoStart`, `autoTrackPageviews`, `trackQueryChanges`, and `trackHashChanges`) still affect only page analytics. GPC, URL capture, mock, debug, and endpoint settings apply to all three modules. Function-based `enrichRule` still requires the module API; it cannot be supplied as an HTML attribute.

After the script loads, use the same globals as the individual bundles:

```js
window.OwlEyeAnalytics.track("signup_completed", { plan: "free" });
const end = window.OwlEyePerformance.start("report_generation");
// Call after the operation completes:
end({ status: "ok" });
```

Rules and Web Vitals start on load. `data-owleye-auto-start="false"` only delays page analytics; it is not a gate for the whole bundle. If all collection must wait, load the full script only after that decision. Loading full plus individual bundles does not duplicate automatic trackers for the same Tracking ID and endpoint, but normally choose one approach to avoid redundant downloads.

To clean up the automatic trackers:

```js
window.OwlEyeAnalytics.stop();
window.OwlEyeRules.stop();
window.OwlEyePerformance.observeVitals().stop();
```

There is no aggregate `OwlEyeFull` controller or global performance `stop()`. Do not invoke previously returned custom timer end functions after collection permission is withdrawn. Stopping trackers does not retract requests already started.

Remove legacy `data-owleye-capture-campaigns` attributes when upgrading; they are ignored, even when set to `false`. Review visitor notices for automatic campaign capture, and keep the frontend SDK aligned with the matching API deployment.

## Script attributes

| Attribute | Default | Meaning |
| --- | --- | --- |
| `data-owleye-id` | Required | Public app Tracking ID |
| `data-owleye-server` | Hosted service | Custom API origin or API-base override; requires a correctly configured endpoint |
| `data-owleye-auto-start` | `true` | Start page analytics immediately; only affects the analytics bundle |
| `data-owleye-capture-query` | `false` | Include the full query string; review its contents first |
| `data-owleye-capture-hash` | `false` | Include URL fragments |
| `data-owleye-respect-global-privacy-control` | `true` | Stop tracking for Global Privacy Control in the SDK; the API does not filter the header |
| `data-owleye-auto-track-pageviews` | `true` | Disable automatic views for manual/memory-router navigation |
| `data-owleye-track-query-changes` | `false` | Count query-only navigation; does not enable query capture |
| `data-owleye-track-hash-changes` | `false` | Count hash-only navigation; does not enable fragment capture |
| `data-owleye-debug` | `false` | Explain delivery decisions and numeric acknowledgements without payloads |
| `data-owleye-mock` | `false` | Do not send requests |

Use explicit `"true"` or `"false"` values. `data-owleye-server` is an advanced endpoint override, not a hosted setup requirement or a supported first-party proxy product.

## Endpoint overrides are not a proxy product

Hosting the script yourself changes where JavaScript is downloaded. `data-owleye-server` changes where API requests are sent. Neither guarantees blocker recovery or makes a generic rewrite preserve visitor identity and geography.

Owleye Analytics trusts a forwarded client address only when the immediate peer is configured as a trusted proxy by the API operator. That operator must control the hop and sanitize client-supplied forwarding headers. Hosted customers cannot establish this trust merely by changing a script attribute. An untrusted proxy can make visitors share the proxy address, distorting visitor/session counts, geography and rate limits.

A proxy also adds latency, bandwidth cost, possible request logging and another failure point. Keep the normal hosted origin unless the routing and trust chain have been explicitly tested with the service operator. Preserve permission decisions and Global Privacy Control behavior; endpoint changes are not a mechanism for overriding visitor choices. Owleye Analytics does not currently offer a managed first-party proxy setup.

## Consent and cleanup

If your site must wait for permission, load scripts only after that decision. For page analytics alone, `data-owleye-auto-start="false"` creates a controller without starting collection; call `window.OwlEyeAnalytics?.start()` when allowed, and `.stop()` on withdrawal.

The rules and performance scripts start their respective modules on load. Delaying the analytics script alone does **not** delay them. For precise lifecycle ownership, use the [package entrypoints](/docs/sdk/) instead. Web Vitals `stop()` discards pending measurements and removes observers/listeners; it does not retract requests already started. Hidden/page-exit events flush lifecycle values only while observation remains active.

## Controllers and TypeScript

Each successfully initialized bundle exposes a controller:

| Global | Methods |
| --- | --- |
| `window.OwlEyeAnalytics` | `track()`, `pageview()`, `setGlobalRecords()`, `start()`, `stop()` |
| `window.OwlEyeRules` | `stop()` |
| `window.OwlEyePerformance` | `start()` for a manual timer, `observeVitals()` for the shared Web Vitals controller |

For example, after the performance bundle loads:

```js
const end = window.OwlEyePerformance?.start("report_generation");
// After your operation finishes:
end?.({ status: "ok" });
```

`observeVitals()` returns the existing active observer when called after bundle initialization; retain that controller and call its `stop()` during teardown. The performance global itself has no `stop()` method. Rules cannot be restarted through their stopped controller; module imports give explicit control over reinitialization.

The script bundles do not install global TypeScript declarations in your app. If TypeScript code accesses these globals, install the package for its types and add an application declaration file:

```ts
import type { AnalyticsController } from "@owleye/analytics";
import type { RulesController } from "@owleye/analytics/rules";
import type { PerfController } from "@owleye/analytics/performance";

declare global {
  interface Window {
    OwlEyeAnalytics?: AnalyticsController;
    OwlEyeRules?: RulesController;
    OwlEyePerformance?: PerfController;
  }
}
```

These optional properties reflect that a script can be blocked or not loaded yet. Type-only imports do not initialize another SDK copy. Prefer regular module imports when using a bundler.

## Verify

Check browser Network requests to `/v1/events`; confirm `accepted` is greater than zero. If requests are missing, check content blockers, browser privacy signals, CSP, and whether the script actually loaded. If you restrict allowed origins in app Settings, include every intended origin. See [WordPress](/docs/wordpress/) for CMS-specific placement and caching checks.

For memory routers, disable automatic pageviews and call `window.OwlEyeAnalytics.pageview({ path, title })` once per committed screen, including the initial screen. See [SPA and memory routers](/docs/sdk/#spa-and-memory-routers).