> ## Documentation Index
> Fetch the complete documentation index at: https://docs.davazmysel.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Install the tracking snippet

> Add one script tag to your landing and destination pages to track visits across all your domains.

The tracking snippet records what happens after the redirect — page views on your landing and destination pages. You install it once, and the same snippet works across all domains and subdomains in your workspace.

## Get your snippet

Go to **Settings → Workspace**, open the **Tracking** tab, and copy the snippet from the **Tracking snippet** card. It looks like this:

```html theme={null}
<script src="https://cdn.davazmysel.com/t.js" data-site="acct_a1b2c3d4" defer></script>
```

The `data-site` value is your workspace's unique ID — always copy the snippet from the dashboard rather than retyping it. If you only need the bare ID (for a platform install recipe or a customer-operated Worker proxy, both below), grab it directly from the **Site ID** card under **Settings & Integrations** instead of extracting it from the full snippet.

## Install it

Paste the snippet into the `<head>` of **every page** of your landing and destination sites. If your site uses a tag manager or a shared layout template, one paste covers everything.

<Note>
  On [hosted pages](/domains/hosted-pages) (Mode D), the snippet is injected automatically — you only need to install it on your own sites.
</Note>

## What it tracks

* **Page views**, including client-side navigation in single-page apps.
* **Clicks** on links and buttons.
* **Form submissions** — emails are hashed in the browser before being sent.

## Options

Configure behavior with attributes on the script tag:

| Attribute              | Default | Effect                                       |
| ---------------------- | ------- | -------------------------------------------- |
| `data-track-cta`       | on      | Track clicks on links and buttons            |
| `data-track-forms`     | on      | Track form submissions                       |
| `data-require-consent` | off     | Hold all tracking until the visitor consents |
| `data-debug`           | off     | Log snippet activity to the browser console  |

## Consent mode

If your audience requires consent (GDPR and similar), enable **Require consent before tracking** under **Settings → Workspace → Settings & Integrations** (Consent card) — it's a single workspace-wide toggle, not set per domain. The snippet then stays completely silent — no events, no cookies — until a `dava-consent=true` cookie exists.

The dashboard generates a ready-made consent banner you can customize and paste next to the snippet. Any third-party consent tool (CookieBot, OneTrust, …) works too, as long as it sets the `dava-consent` cookie and calls:

```js theme={null}
window.dava.checkConsent()
```

## Verify the install

Open one of your pages in the browser, then check the dashboard — the visit should appear within seconds. If nothing shows up, add `data-debug="true"` to the script tag and check the browser console for errors.

## Install method 2: Customer-operated Cloudflare Worker proxy

Deploy a Cloudflare Worker in your own account that proxies the tracking script and event endpoint through a **dedicated subdomain** (e.g. `collect.yourdomain.com`). This avoids the browser loading anything from `cdn.davazmysel.com`, without touching the DNS/proxy configuration of your main site at all — sometimes called a "first-party proxy" or "server-side install".

<Note>
  **Why a dedicated subdomain, not a path on your main domain.** An earlier version of this guide recommended routing `yourdomain.com/_dava/*` through a Cloudflare Workers Route on your existing domain. In practice this breaks silently for any site fronted by another CDN/host in front of Cloudflare (Cloudflare nameservers assigned but the DNS record itself set to "DNS only", with a different edge — e.g. WP Engine's own Cloudflare-based edge — actually serving traffic). A brand-new subdomain bound via **Cloudflare Custom Domain** sidesteps this entirely: it does not depend on your root domain's existing proxy status, and Cloudflare provisions the DNS record and SSL certificate for you.
</Note>

### Prerequisites

* A Cloudflare account with owner or admin access (a zone for your domain must already exist in it — Custom Domains require the zone, even though you are only adding a new subdomain).
* [`wrangler` CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/) installed (run `npm install -g wrangler` or `pnpm add -g wrangler`, then `wrangler login`).

### Setup

<Steps>
  <Step title="Create a Worker project">
    In a new directory, scaffold a Worker:

    ```bash theme={null}
    wrangler init dava-proxy
    cd dava-proxy
    ```

    When prompted, accept the defaults. This creates a `wrangler.jsonc` and a starter `src/index.ts` — replace both with the files below. (Do not also keep a `wrangler.toml` alongside `wrangler.jsonc` — Wrangler silently prefers one over the other if both exist, which is a confusing way to deploy the wrong file.)
  </Step>

  <Step title="Paste the template files">
    Copy the `wrangler.jsonc` and `src/index.ts` shown below into your project, replacing the generated files.

    In `wrangler.jsonc`'s `vars`, set `DAVA_WORKER_URL` to the Worker endpoint URL shown in your Dáva workspace under **Settings → Workspace → Settings & Integrations**. `DAVA_SNIPPET_URL` is the same public CDN URL as in the standard script tag — leave it as-is. In `src/index.ts`'s `ALLOWED_ORIGINS`, list every real origin your pages are served from (bare domain, `www`, and any other app subdomains that will load the script) — this is a genuine cross-origin call now, and only listed origins get a valid CORS response.
  </Step>

  <Step title="Deploy">
    ```bash theme={null}
    wrangler deploy
    ```
  </Step>

  <Step title="Add a Custom Domain to the Worker">
    In the Cloudflare dashboard, go to your Worker (**Workers & Pages → dava-proxy → Settings → Domains & Routes**) and **Add Custom Domain** — enter your dedicated subdomain, e.g. `collect.yourdomain.com`. Cloudflare creates the DNS record and issues the SSL certificate automatically; no manual DNS step needed. If a stale DNS record already exists for that exact subdomain, delete it first or the Custom Domain add will fail with a conflict.
  </Step>

  <Step title="Update your script tag">
    Replace the standard Dáva snippet on every page with one that loads through your Worker proxy, using the **full absolute URL** of your dedicated subdomain — a relative path here silently resolves against your page's own origin instead, which 404s:

    ```html theme={null}
    <script
      src="https://collect.yourdomain.com/_dava/listen.js"
      data-site="acct_a1b2c3d4"
      data-endpoint="https://collect.yourdomain.com/_dava/events"
      defer
    ></script>
    ```

    The `data-endpoint` attribute is required, and its path must be **exactly `/_dava/events`** — the same value as `EVENTS_PATH` in the Worker above. There are two ways to get this wrong, and both silently lose every event:

    * **Leaving it out.** The snippet falls back to the page's own origin plus its default `/t/event` path, which hits your origin server instead of your Worker.
    * **Gluing that default path onto the proxy prefix** — i.e. `https://collect.yourdomain.com/_dava/t/event`. The Worker does not handle that path, so the request falls through to the "pass everything else through to your origin" branch and dies there. Use `/_dava/events`, never `/_dava/t/event`.

    <Warning>
      Both mistakes look healthy at a glance: the preflight `OPTIONS` returns **204** (the Worker answers `OPTIONS` on every path), while the actual `POST` fails with `net::ERR_FAILED`. If you see that exact pair in DevTools, check the `data-endpoint` path before anything else.
    </Warning>
  </Step>
</Steps>

**`wrangler.jsonc`** — deploy under any Worker name you like:

```jsonc theme={null}
{
  "name": "dava-proxy",
  "main": "src/index.ts",
  "compatibility_date": "2026-07-18",
  "vars": {
    // Paste the value from Dáva workspace settings → Settings & Integrations → Worker endpoint URL.
    "DAVA_WORKER_URL": "https://dava-worker.YOUR_CF_ACCOUNT.workers.dev",
    // Dáva's public snippet CDN — same URL as the standard script tag.
    "DAVA_SNIPPET_URL": "https://cdn.davazmysel.com/t.js"
  }
}
```

**`src/index.ts`**

Requires `"types": ["@cloudflare/workers-types"]` in your `tsconfig.json` (added automatically by `wrangler init`).

```ts theme={null}
// Paths this Worker intercepts — change to whatever you prefer.
// Use the same values in the snippet's src attribute and data-endpoint on your pages.
const LISTEN_PATH = "/_dava/listen.js";
const EVENTS_PATH = "/_dava/events";

// This Worker is bound to a DEDICATED subdomain (Cloudflare Custom Domain), not
// your main site — so requests from the real page arrive cross-origin. List every
// real origin your pages are served from.
const ALLOWED_ORIGINS = new Set([
  "https://yourdomain.com",
  "https://www.yourdomain.com",
]);

export interface Env {
  /** Dáva event Worker endpoint — copy from workspace settings → Settings & Integrations. */
  DAVA_WORKER_URL: string;
  /** Dáva snippet CDN. Default: https://cdn.davazmysel.com/t.js */
  DAVA_SNIPPET_URL: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const origin = request.headers.get("Origin");

    if (request.method === "OPTIONS") {
      return new Response(null, { status: 204, headers: corsHeaders(origin) });
    }

    if (url.pathname === LISTEN_PATH) {
      return proxySnippet(env);
    }

    if (url.pathname === EVENTS_PATH && request.method === "POST") {
      // The real page hostname (e.g. yourdomain.com) is NOT url.hostname here —
      // this Worker sits on its own dedicated subdomain. Derive it from Origin
      // instead. Not required for account-wide-pixel installs (data_site in the
      // payload bypasses this entirely), kept for defense-in-depth.
      const pageHostname = origin ? new URL(origin).hostname : url.hostname;
      return proxyEvent(request, env, pageHostname, origin);
    }

    // All other paths pass through to your origin unchanged.
    return fetch(request);
  },
} satisfies ExportedHandler<Env>;

function corsHeaders(origin: string | null): HeadersInit {
  if (!origin || !ALLOWED_ORIGINS.has(origin)) return {};
  return {
    "Access-Control-Allow-Origin": origin,
    // Required: the snippet's sendBeacon fallback (clicks, form_submits, and the
    // fetch-failure retry) is ALWAYS credentialed, so the browser demands
    // Allow-Credentials: true on both the preflight and the response — omit it and
    // every beacon-delivered event is silently dropped by CORS. Safe with a
    // specific (non-"*") Allow-Origin, which is exactly what we echo above.
    "Access-Control-Allow-Credentials": "true",
    "Access-Control-Allow-Methods": "POST, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type",
    "Access-Control-Max-Age": "86400",
    Vary: "Origin",
  };
}

/** 1:1 passthrough of the Dáva snippet from the CDN. No CORS needed — <script src> is not origin-restricted. */
async function proxySnippet(env: Env): Promise<Response> {
  const upstream = await fetch(env.DAVA_SNIPPET_URL);
  return new Response(upstream.body, {
    status: upstream.status,
    headers: upstream.headers,
  });
}

/**
 * Forward a browser event to Dáva's event Worker.
 *
 * X-Forwarded-Host is set for defense-in-depth (non-data_site installs); it is
 * NOT required for the account-wide-pixel (data_site) install used above.
 *
 * User-Agent and X-Forwarded-For MUST be forwarded — Dáva parses
 * device_type/browser/os from User-Agent and truncates client_ip from the
 * first hop of X-Forwarded-For. Without them, Dáva sees this Worker's own
 * outbound fetch() as "the request" and every proxied event silently loses/
 * corrupts those fields (they land as null/null/"desktop" regardless of the
 * real visitor's device — confirmed in production, not a hypothetical).
 *
 * Use X-Forwarded-For, NOT CF-Connecting-IP, to carry the visitor's IP. This
 * Worker → Dáva's Worker is a CROSS-ZONE subrequest (different Cloudflare
 * accounts) — Cloudflare unconditionally overwrites any CF-*-prefixed header
 * on a cross-zone subrequest with its own internal value (anti-spoofing), so
 * setting CF-Connecting-IP here is silently discarded before Dáva ever sees
 * it. Confirmed by tracing a live test event through to Dáva's DB (landed
 * with client_ip_truncated null and country resolved to wherever Cloudflare
 * routed this Worker's own connection to Dáva, not the visitor's, despite
 * this header being set). X-Forwarded-For has no such restriction and
 * survives the hop intact — the same reason Dáva's own Caddy relay (for
 * customer-CNAME installs) uses X-Forwarded-For rather than CF-Connecting-IP.
 */
async function proxyEvent(
  request: Request,
  env: Env,
  pageHostname: string,
  origin: string | null,
): Promise<Response> {
  const forwardHeaders = new Headers({
    "Content-Type": "application/json",
    "X-Forwarded-Host": pageHostname,
  });
  const userAgent = request.headers.get("User-Agent");
  if (userAgent) forwardHeaders.set("User-Agent", userAgent);
  const clientIp = request.headers.get("CF-Connecting-IP");
  if (clientIp) forwardHeaders.set("X-Forwarded-For", clientIp);

  const upstream = await fetch(
    new Request(`${env.DAVA_WORKER_URL}/t/event`, {
      method: "POST",
      headers: forwardHeaders,
      body: request.body,
    }),
  );

  // Dáva's own backend already sets its own CORS headers (echoing Origin, or
  // falling back to "*" since this server-to-server call carries no Origin
  // header). Strip those before applying ours, or the browser sees two
  // conflicting Access-Control-Allow-Origin values and rejects the response
  // outright — a plain object spread does not overwrite same-name headers
  // cleanly, it appends, producing an invalid combined value like
  // "*, https://yourdomain.com".
  const headers = new Headers(upstream.headers);
  headers.delete("Access-Control-Allow-Origin");
  headers.delete("Access-Control-Allow-Credentials");
  headers.delete("Access-Control-Allow-Methods");
  headers.delete("Access-Control-Allow-Headers");
  headers.delete("Vary");
  for (const [key, value] of Object.entries(corsHeaders(origin))) {
    headers.set(key, value as string);
  }
  return new Response(upstream.body, { status: upstream.status, headers });
}
```

### Caveats and limits

<Warning>
  **Forward the real request's headers — a proxy that drops them corrupts or silently loses data.**

  Three separate, confirmed-in-production failure modes, all from the same root cause (a header the browser sent was not forwarded to Dáva):

  * **Missing/wrong `X-Forwarded-Host`** (only relevant for non-`data_site` installs) → `200 filtered: "no_hostname"` or `"unknown_domain"`, event discarded silently.
  * **Missing `User-Agent`** → the event is still accepted, but `device_type`/`browser`/`os` land as `null`/`null`/`"desktop"` regardless of the visitor's real device — Dáva parses these from `User-Agent` on whatever request it receives, and without forwarding, that request is this Worker's own `fetch()` call, not the browser's.
  * **Sending the visitor's IP as `CF-Connecting-IP` instead of `X-Forwarded-For`** → silently dropped. This Worker calling Dáva's Worker is a cross-zone subrequest (different Cloudflare accounts) — Cloudflare unconditionally overwrites any `CF-*`-prefixed header on a cross-zone subrequest with its own internal value (anti-spoofing), so Dáva never sees what you set — `client_ip_truncated` lands `null` and geo (`cf_country`) falls back to wherever Cloudflare routed this Worker's own connection to Dáva, not the visitor's. Forward it as `X-Forwarded-For` instead (see the template above) — that header name has no such restriction and survives the hop.

  To debug missing or malformed events, open your Worker's real-time log in the Cloudflare dashboard (**Workers & Pages → dava-proxy → Observability**).
</Warning>

<Warning>
  **CORS must allow every real origin your pages load from.** Because the Worker sits on a dedicated subdomain, the browser's request to `/_dava/events` is cross-origin. If `ALLOWED_ORIGINS` doesn't include the exact origin the page is served from (check `www` vs bare domain, and any other app subdomains), the browser blocks the response even though Dáva accepted the event server-side — the request still lands in Dáva (so DB inspection can mislead you into thinking it "worked"), but your own JS never sees a successful response. Check the browser console for `Access-Control-Allow-Origin` errors, not just the network tab's status code.
</Warning>

<Note>
  **Cloudflare Workers free-tier limit.** The free plan allows 100,000 Worker invocations per day. A typical page view generates two invocations (one script load, one event POST), and a page with CTA or form tracking may generate three or more. A site with a few thousand daily visitors can exhaust the free quota within hours. Check your traffic before relying on the free tier; a paid Cloudflare Workers plan removes the daily cap.
</Note>

<Note>
  **Domain ownership.** Cloudflare's Custom Domain feature proves you control the zone your collector subdomain lives in — but that only covers the collector subdomain itself, not the domain your actual pages are served from. The `ALLOWED_ORIGINS` allowlist in the Worker is the real boundary here: only requests whose `Origin` header matches an entry you listed get a valid CORS response and get forwarded. Keep that list to origins you actually control.
</Note>
