[invisit](https://invisit.dev) · [Docs](https://invisit.dev/docs) · [llms.txt](https://invisit.dev/llms.txt) · [llms-full.txt](https://invisit.dev/llms-full.txt)

# Installation

> Put the tracker on your site with one script tag and no build step, or with the typed, SSR-safe @invisitdev/web package.

Source: https://invisit.dev/docs/web/installation

There are two ways to install invisit, and they run the same tracker built from the same source. Pick the **script tag** when there is no build step: static HTML, WordPress, Shopify, Framer. It works in bundled apps too. Pick the **npm package** when you want `track()`, `identify()` and `consent()` as typed imports and the tracker bundled with your app.

Both need your **site id**, from **Settings → Installation** in the dashboard. It is public and never changes, so it is safe in client code and in `PUBLIC_` or `NEXT_PUBLIC_` environment variables.

## Script tag

```html title="index.html"
<script
  async
  src="https://api.invisit.dev/invisit.js"
  data-site="<site id>"
  data-collector="https://api.invisit.dev"
></script>
```

The tracker loads asynchronously and never blocks the page. The tag always serves the current tracker release, so updates reach your site with no change on your side. Where it goes on WordPress, Shopify and Framer is in the [platform guides](https://invisit.dev/docs/web/frameworks.md#wordpress).

> [!IMPORTANT]
> Keep `data-collector`. Unlike the npm package, the script has no default collector: without the attribute it sends events to your own origin, which only works behind the [first-party proxy](https://invisit.dev/docs/server/sdk.md).

## npm package

```sh title="Terminal"
npm install @invisitdev/web
```

Or with `pnpm add`, `yarn add` or `bun add`. Then call `init()` once, in the browser, as early as your app allows:

```ts title="src/main.ts"
import { init } from '@invisitdev/web'

init({ site: '<site id>' })
```

That is the whole setup. `init()` sends the first page view, follows client-side navigation from then on, and returns the running tracker. The [web frameworks](https://invisit.dev/docs/web/frameworks.md) show where that line goes in each framework.

The package's optional parts (web vitals, no-code rules, the feedback widget, the heatmap overlay) are dynamic imports, so your bundler splits them into your own files and the page never requests a script from a third-party host.

> [!NOTE]
> Importing the package touches no browser globals, and every function is a silent no-op where there is no `window`. Import it from shared or server code freely: it only does something once `init()` runs in the browser.

Calls made before `init()` are queued and applied once it starts, so an `identify()` in your auth code can safely run first.

## Options

Each option is an `init()` field, or a `data-*` attribute on the script tag. Boolean attributes take `true` or `false`.

| Option           | Attribute              | Default                                        | What it does                                                                                                            |
| ---------------- | ---------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `site`           | `data-site`            | required                                       | The site id.                                                                                                            |
| `baseUrl`        | `data-collector`       | `https://api.invisit.dev`, none for the script | The collector, or a [proxy](https://invisit.dev/docs/server/sdk.md) path such as `/_i`.                                                       |
| `requireConsent` | `data-require-consent` | `false`                                        | Sends nothing until consent is granted. See [Privacy](https://invisit.dev/docs/guides/privacy.md#requiring-consent).                          |
| `storage`        | `data-storage`         | `auto`                                         | `none` keeps nothing at all on the device.                                                                              |
| `excludePaths`   | `data-exclude-paths`   | none                                           | Paths never measured. See [Excluding traffic](https://invisit.dev/docs/guides/privacy.md#excluding-traffic).                                  |
| `outboundLinks`  | `data-outbound-links`  | `true`                                         | Records clicks on links to other sites.                                                                                 |
| `fileDownloads`  | `data-file-downloads`  | `true`                                         | Records clicks on file downloads.                                                                                       |
| `respectGpc`     | `data-respect-gpc`     | `false`                                        | Turned on, stops for browsers that send Global Privacy Control.                                                         |
| `respectDnt`     | `data-respect-dnt`     | `false`                                        | Turned on, stops for browsers that send Do Not Track.                                                                   |
| `debug`          | `data-debug`           | `false`                                        | Logs delivery problems to the console, and lets a browser driven by automation send events, so you can test your setup. |

Everything else (allowed domains, redacted query keys, web vitals, heatmaps, no-code event rules, the feedback widget) is set per site in the dashboard and fetched by the tracker when it starts, so changing it needs no redeploy.

## Group dynamic paths

Where ids or names sit in the path, such as `/orders/8121` or `/acme/settings`, every one of them
is its own row in the Pages report. `pageUrl` rewrites each address before it is recorded, so one
route is one row:

```ts title="src/main.ts"
init({
  site: '<site id>',
  pageUrl: (href) => href.replace(/\/orders\/\d+/, '/orders/:id'),
})
```

It gets the full URL and returns a full URL; query parameters and the fragment are redacted
afterwards as usual. With the script tag, queue the same function on the `invisit` stub before the
script loads:

```html title="index.html"
<script>
  window.invisit =
    window.invisit ||
    function () {
      ;(invisit.q = invisit.q || []).push(arguments)
    }
  invisit('pageUrl', (href) => href.replace(/\/orders\/\d+/, '/orders/:id'))
</script>
<script
  async
  src="https://api.invisit.dev/invisit.js"
  data-site="<site id>"
  data-collector="https://api.invisit.dev"
></script>
```

Two addresses that rewrite to the same one count as the same page, so moving between them records
no new page view.

A heatmap is drawn over the page opened at its recorded address, and `/orders/:id` opens no page.
So clicks are not collected where `pageUrl` changes the path, and those pages have no click
heatmap. Rewriting only the query keeps it.

## Both on one page

If a page has both the script tag and the package, whichever starts first wins and the other reuses it. A visit is never counted twice.

## Single-page apps

Nothing to add. The tracker follows client-side navigation in every router and records a page view for each new address. The fragment is not part of an address, so a router that changes only `#/path` records one page view, not one per route.

## Browser support

Chrome 91, Edge 91, Firefox 90, Safari 15 and later.
