# Add the script

> Paste odo’s script into every page: the tag and its options, leaving out your own visits, cookieless mode, the early snippet, and ad blockers.

odo measures your website with one small script of its own: no other company’s code, and no tag manager needed.

## Add your website

In odo, open **Settings → Websites** and add your site’s domain. odo gives the site an id and shows its tag. Paste the tag into the `<head>` of every page:

```html
<script defer src="https://odo.gg/js/odo.js" data-site="SITE_ID"></script>
```

The tag in odo has your site’s id in place of `SITE_ID`. Subdomains count as the same site, so `www.` and `app.` need nothing more. Other hosts that should count too (a second domain, a staging host) and paths to leave out (`/admin/*`) are set per site, in the same place.

The script is deferred, so it never holds up your page, and it sends what it measures with `sendBeacon`, so a visitor leaving doesn’t lose their last page.

## Options on the tag

| Attribute | What it does |
| --- | --- |
| `data-site` | Required. The site’s id. |
| `data-api` | Where hits go, when you [serve odo from your own domain](https://odo.gg/docs/add-the-script#ad-blockers): `/odo/api/event`. The rest of what the script asks odo for comes from the same path. |
| `data-hash` | Counts `#/routes` as separate pages, for apps that route with the hash. |
| `data-local` | Also measures on `localhost`, `127.0.0.1` and `.local` hosts. Off by default, so development doesn’t count. |
| `data-cookieless` | Never stores an id in the browser. See below. |
| `data-capture` | Which of `clicks`, `forms`, `errors`, `sections`, `vitals` (page speed) and `heat` ([heatmaps](https://odo.gg/docs/heatmaps)) to record, or `none`. All of them without it. See [clicks, forms and errors](https://odo.gg/docs/interactions). |

## Leave out your own visits

Open your site once with `?odo=ignore` on the address: `https://example.com/?odo=ignore`. That browser stops being counted on that site, and its console says so. `?odo=track` counts it again.

Automated browsers aren’t counted either: headless Chrome, Lighthouse, Cypress, PhantomJS and any browser that says a program is driving it. A page the browser prerenders counts only once someone actually sees it.

## Cookieless, and Global Privacy Control

By default the script keeps one first-party id, `odo_vid`, in a cookie and in local storage on your own domain. It’s how odo recognizes a returning visitor, keeps each one on the same version of an [experiment](https://odo.gg/docs/experiments), and lets a checkout pass the visitor to your payment platform, so the sale is [traced to the visit](https://odo.gg/docs/sales-to-visits).

A cookieless site stores nothing in the browser. Turn on **Cookieless** in the website’s settings and add `data-cookieless` to the tag (only the tag stops a browser that already has the script from storing the id):

```html
<script defer src="https://odo.gg/js/odo.js" data-site="SITE_ID" data-cookieless></script>
```

Visitors are then counted by a hash of the site, their IP address and their browser, salted with a secret that’s replaced every day and then deleted, so one day’s visits can’t be linked to the next. A returning visitor is recognized within a day only, sales can’t be traced to visits, and experiments don’t run.

Browsers that send Global Privacy Control are treated that way on every site, whatever its setting. More in [privacy and data](https://odo.gg/docs/privacy).

## The early snippet

Because the script is deferred, your own code can run before it. If that code calls `odo()` or `odo.experiment()`, paste the early snippet right after odo’s tag and before your own scripts. It answers `odo.experiment()` at once, with the variant odo.js will give, queues events until the script arrives, and keeps working where the script is blocked.

```html
<script>"use strict";(()=>{var T={perBeacon:30,perPage:100,errorsPerPage:10,label:120,element:120,section:80,maxAgoMs:15*6e4,experiment:60,variant:40},m=/^[\w .:-]{1,60}$/,f=/^[\w .:-]{1,40}$/;function _(t){let e=2166136261;for(let n=0;n<t.length;n++)e^=t.charCodeAt(n),e=Math.imul(e,16777619);return e>>>0}function x(t,e,n,i){if(!n.length)return null;let u=_(`${t}:${e}`)/4294967296,r=i&&i.length===n.length&&i.every(o=>Number.isFinite(o)&&o>=0)&&i.some(o=>o>0)?i:n.map(()=>1),c=r.reduce((o,l)=>o+l,0),s=0;for(let o=0;o<n.length;o++)if(s+=r[o]/c,u<s)return n[o];return n[n.length-1]}var S="[data-odo-variant][hidden]{display:none!important}";function p(t=S){try{let e=document.createElement("style");document.head.appendChild(e),e.sheet.insertRule(t)}catch(e){}}function h(t,e,n,i,u){let a=typeof e=="string",r=a?[e]:e||[];if(!m.test(t)||!r[0]||!r.every(o=>f.test(o)))return{variant:r[0]||null};let c=(u.find(o=>o.startsWith(`${t}:`))||"").slice(t.length+1);if(c&&(a||r.includes(c)))return{variant:c,valid:!0};let s=a?e:i&&x(i,t,r,n);return s?{variant:s,exposed:!0,valid:!0}:{variant:r[0],valid:!0}}var y=()=>(new URLSearchParams(location.search).get("odo_variant")||"").split(",");var d={get(t){try{return localStorage.getItem(t)}catch(e){return null}},set(t,e){try{e===null?localStorage.removeItem(t):localStorage.setItem(t,e)}catch(n){}}},w=()=>(/(?:^|;\s*)odo_vid=([\w-]{12,40})/.exec(document.cookie)||[])[1];var I=()=>{var t;return(t=window.crypto)!=null&&t.randomUUID?window.crypto.randomUUID().replace(/-/g,""):Array.from({length:24},()=>(Math.random()*36|0).toString(36)).join("")};function E(){let t=w()||d.get("odo_vid");return t&&/^[\w-]{12,40}$/.test(t)?t:null}(()=>{let t=window,e=document,n=t.odo||function(){n.q=n.q||[],n.q.push(arguments)};if(n.experiment)return;let i=[];n.x=i,n.experiment=(a,r,c)=>{let s=t.odo;if(s!==n&&s.experiment)return s.experiment(a,r,c);let o=!navigator.globalPrivacyControl&&!e.querySelector("script[data-site][data-cookieless]"),l=o?E():null;o&&!l&&e.querySelector("script[data-site]")&&(l=I(),d.set("odo_vid",l));let{variant:g,exposed:b}=h(a,r,c,l,y());return b&&i.push([a,g,typeof r=="string"?"":l]),g},t.odo=n,p();let u=d.get("odo_h");u&&(e.documentElement.dataset.odoPath=location.pathname,u.split("	").map(p))})();})();</script>
```

It also keeps the elements that [experiments odo runs](https://odo.gg/docs/experiments#without-flicker) change out of sight until odo has changed them, for 800 ms at most, so returning visitors don’t see the original flash first. Without experiments, you don’t need it. It’s in **Settings → Websites** in odo too. If your site sends a Content Security Policy, allow this inline script by its hash.

## Calling odo before it loads

Without the early snippet, this one line queues calls until the script arrives:

```html
<script>window.odo=window.odo||function(){(odo.q=odo.q||[]).push(arguments)}</script>
```

Once the script has run, `window` hears an `odo:ready` event.

## Ad blockers

Some blockers stop analytics scripts that load from another domain. Serve odo from your own domain instead: forward everything under one path of your site, such as `/odo/`, to `https://odo.gg/`, so `/odo/js/odo.js` reaches `https://odo.gg/js/odo.js` and `/odo/api/event` reaches `https://odo.gg/api/event`. odo answers under `https://odo.gg/odo/` too, so a proxy that keeps the `/odo` works as well.

With Next.js, in `next.config.js` (`vercel.json` takes the same `rewrites`):

```js
module.exports = {
  async rewrites() {
    return [{ source: '/odo/:path*', destination: 'https://odo.gg/:path*' }];
  },
};
```

With Netlify, in `_redirects`:

```
/odo/*  https://odo.gg/:splat  200
```

With nginx:

```nginx
location /odo/ {
  proxy_pass https://odo.gg/;
  proxy_ssl_server_name on;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```

Then load the script from there:

```html
<script defer src="/odo/js/odo.js" data-site="SITE_ID" data-api="/odo/api/event"></script>
```

Everything else the script asks odo for comes from the same path, read from `data-api` (what comes before `api/event`), or from where the script was loaded: the [heatmap](https://odo.gg/docs/heatmaps) overlay and the heatmap it draws (`/odo/js/heatmap.js`, `/odo/api/heatmap`), and what [experiments odo runs](https://odo.gg/docs/experiments#experiments-odo-runs) need (`/odo/js/odo-x.js`, `/odo/js/x/…`). Forwarding the whole path covers whatever odo adds later too.

A proxy that forwards only `/odo/js/odo.js` and `/odo/api/event` works too. Visits and events still go through your domain, and heatmaps and experiments odo runs load from `https://odo.gg` itself, so ad blockers and a Content-Security-Policy can stop those.

## Next

[Connect your sales](https://odo.gg/docs/connect-your-sales), then [check it’s working](https://odo.gg/docs/check-its-working).
