# Add CLItrail to Vanilla JavaScript (ES modules)

Where the CLItrail tag goes in Vanilla JavaScript (ES modules), in its own idiom, with the consent, CSP and client-side routing details and how to check it works.

Written for Any bundler, or native modules

## Where the tag goes

Edit `index.html and src/main.js`. Put the tag on every page that shows your install command; site-wide is best, after your existing analytics tags.

```html
<script defer src="https://YOUR_SERVICE/browser.js"></script>
<script type="module" src="/src/main.js"></script>
```

Start it from your code

```js
// Module scripts run after the deferred scripts before them, so the SDK is loaded here.
// Without data-website the SDK does nothing until init runs.
const sdk = window.InstallAttribution;
if (sdk) sdk.ready = sdk.init({ service: 'https://YOUR_SERVICE', project: 'YOUR_WEBSITE_ID' });
```

## Only after consent

Use this in place of the start code in `main.js`. Loaded without `data-website`, `browser.js` reads, stores and sends nothing until `init` runs. Call `startClitrail()` once the visitor agrees; the `ready` check keeps it to one visit per page load.

```js
// Call startClitrail() from your consent manager's "accepted" callback.
export function startClitrail() {
  const sdk = window.InstallAttribution;
  if (sdk && !sdk.ready) sdk.ready = sdk.init({ service: 'https://YOUR_SERVICE', project: 'YOUR_WEBSITE_ID' });
}
```

## Client-side routing

If your own router changes pages with `history.pushState`, call `recordVisit()` after each change (and on `popstate`). The page the browser loaded is recorded by `main.js`, not again.

Route helper

```js
// Records a visit when client-side navigation opens a new page path. The tag records
// the page the browser loaded, so that page is never recorded twice; before the SDK has
// started (for example, before consent) it records nothing.
let recorded;

export function recordVisit() {
  if (typeof window === 'undefined') return;
  recorded ??= new URL(performance.getEntriesByType('navigation')[0]?.name ?? location.href).pathname;
  const sdk = window.InstallAttribution;
  if (!sdk?.ready || location.pathname === recorded) return;
  recorded = location.pathname;
  sdk.ready = sdk.init({ service: 'https://YOUR_SERVICE', project: 'YOUR_WEBSITE_ID' });
}
```

```js
import { recordVisit } from './clitrail.js';

export function navigate(path) {
  history.pushState({}, '', path);
  render(path); // your own view update
  recordVisit();
}

window.addEventListener('popstate', () => {
  render(location.pathname);
  recordVisit();
});
```

## Content Security Policy

Your host's header settings (for example `_headers` on Cloudflare Pages or Netlify, `vercel.json`, or your server), or a `<meta http-equiv="Content-Security-Policy">` tag: add `https://YOUR_SERVICE` to `script-src` and `connect-src`, and `blob:` to `worker-src` (only Safari before 26 needs that one).

```
script-src 'self' https://YOUR_SERVICE;
connect-src 'self' https://YOUR_SERVICE;
worker-src 'self' blob:;
```

## Verify it works

1. Run your dev server or deploy.
2. Open a page with the tag in Chrome or Firefox (accept analytics first if you gate CLItrail on consent), open the developer console and run `await InstallAttribution.ready`. It resolves to `{ ok: true, urlId, hasAnalyticsContext, expiresAt }`.
3. In the CLItrail dashboard, open Visits & identities: the visit is listed.
4. Anything else names the cause: `disabled` (no consent yet, or `data-enabled="false"`), `opfs_unavailable` (the page is not served over https or from localhost), `Error` (the service refused the visit: add the page's origin, including a development origin such as `http://localhost:3000`, under Settings → Additional domains). If `InstallAttribution` is undefined, the tag did not load: check the Network tab and your Content-Security-Policy.

Then run your installer on the same computer and check **Install events**, or run the hook with [`--doctor`](https://clitrail.com/docs/doctor).

## Notes

- Use this when you want to pass options from code: `measurementIds`, `providers`, `getContext`, or adapters registered with `registerAdapter` before `init`.
- Keeping the result in `InstallAttribution.ready` lets `prepareInstall` and the console check use it.

Vanilla JavaScript (ES modules) documentation: [developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)

Using other install paths or destinations? The [setup generator](https://clitrail.com/docs/setup-generator) puts this snippet together with your hooks and destination checklist.
