URL: /guides/pageview-tracking

---
title: Pageview tracking
description: The beacon reference — modes, data-fs attributes, and the observation shape
---

The beacon is a single JavaScript file served at `https://api.formshield.dev/js/formshield.js`. Drop it on a page with a `<script>` tag and it auto-initializes from `data-fs-*` attributes — no extra code. This page is the full reference for those attributes, the three modes, and the observation a hit produces.

The beacon runs only where JavaScript runs, so it never sees pure crawlers. To capture AI agents and bots that fetch HTML without running scripts, pair it with [server reporting](/guides/server-reporting).

## Minimal embed

```html
<script
  async
  src="https://api.formshield.dev/js/formshield.js"
  data-fs-project-key="fs_pub_live_…"
  data-fs-action="pageview"
  data-fs-mode="pageload"
></script>
```

`data-fs-project-key` is the only required attribute. Everything else has a default.

## Modes

`data-fs-mode` selects what the beacon does on load. The default is `auto`.

| Mode | Pageview on load | Form handling | Use when |
| --- | --- | --- | --- |
| `pageload` | Yes — one scored `page.view` | No | You only want pageview analytics. |
| `forms` | No | Yes — attaches to forms | You only want to score form submissions. |
| `auto` (default) | Yes | Yes | You want both from one tag. |

### pageload

Records exactly one scored pageview on window load and does nothing else. This is the recommended mode for traffic analytics.

### forms

Attaches to forms matching `data-fs-form-selector` (default `form`). On submit, the beacon collects signals and injects them as a hidden field named `_fs_signals` (configurable via `data-fs-field`). Your backend forwards that field's JSON to the FormShield check API, which scores the submission.

### auto

Both behaviors: records one pageview on load and attaches to forms. This is the default when `data-fs-mode` is absent or unrecognized.

## Attributes

<ParamField path="data-fs-project-key" type="string" required>
  Your publishable key, `fs_pub_live_…`. Safe to expose in the browser. Create one in the project's Settings.
</ParamField>

<ParamField path="data-fs-mode" type="string" default="auto">
  `pageload`, `forms`, or `auto`. Unknown or absent values fall back to `auto`.
</ParamField>

<ParamField path="data-fs-action" type="string" default="pageview">
  A label for the hit, stored on the observation — for example `pageview`, `signup`, or `contact`. The pageload path defaults to `pageview`. A per-`<form>` `data-fs-action` overrides this global default for that form.
</ParamField>

<ParamField path="data-fs-form-selector" type="string" default="form">
  CSS selector for the forms the beacon attaches to in `forms` and `auto` modes.
</ParamField>

<ParamField path="data-fs-field" type="string" default="_fs_signals">
  Name of the hidden input the beacon injects with the signals payload on form submit.
</ParamField>

<ParamField path="data-fs-init-url" type="string">
  Advanced. Override the handshake endpoint. Defaults to `/v1/handshake` derived from the script's origin.
</ParamField>

<ParamField path="data-fs-collect-url" type="string">
  Advanced. Override the collect endpoint. Defaults to `/v1/collect` derived from the script's origin.
</ParamField>

<ParamField path="data-fs-probe-url" type="string">
  Advanced. Override the client probe endpoint. Defaults are derived from the script's origin.
</ParamField>

<Note>
  The endpoint overrides exist for self-hosted or proxied setups. Leave them unset for the standard `api.formshield.dev` deployment.
</Note>

## What the beacon collects

On a pageview the beacon gathers signals that distinguish a real browser from automation, then sends them to FormShield, which scores the hit and stores the result. The signals include:

- **Browser fingerprint and automation tells** — webdriver, headless, and other automation markers.
- **A signed handshake token** — proves the beacon actually ran in a real browser. Its absence or failure is a strong bot tell on the client path.
- **User-agent classification** — browser, OS, device, and whether the UA is a named bot.

FormShield combines these with server-side IP reputation (VPN, proxy, datacenter, scanner, country, ASN) to produce the final score. The beacon does not collect form field contents on a pageview.

## The observation

Each hit becomes one observation. A clean human pageview:

```json
{
  "score": 0.09,
  "decision": "allow",
  "reasons": [],
  "action": "pageview",
  "ua": {
    "browser": "Chrome",
    "os": "macOS",
    "device": "desktop",
    "bot_kind": null,
    "bot_name": null
  }
}
```

An automated visit (webdriver) crosses the block threshold:

```json
{
  "score": 0.93,
  "decision": "block",
  "reasons": ["automation_detected", "ua_bot"],
  "action": "pageview",
  "ua": {
    "browser": "Chrome",
    "os": "Linux",
    "device": "desktop",
    "bot_kind": "automation",
    "bot_name": null
  }
}
```

### Fields

| Field | Type | Meaning |
| --- | --- | --- |
| `score` | float `0.0`–`1.0` | Risk probability. Higher is more bot-like. |
| `decision` | `allow` \| `review` \| `block` | `allow` below `0.45`, `review` from `0.45`, `block` from `0.8`. |
| `reasons` | string array | Rules that fired (see below). |
| `action` | string | The `data-fs-action` label. |
| `ua` | object | User-agent classification: `browser`, `os`, `device`, `bot_kind`, `bot_name`. |
| `bot_id` | string \| null | The identified bot's stable id, e.g. `googlebot`, `gptbot`. `null` when no bot matched. |
| `bot_operator` | string \| null | The bot's operating company, e.g. `Google`, `OpenAI`. |
| `bot_verified` | `true` \| `false` \| null | `true` = IP-verified; `false` = spoofed (UA claims a bot from the wrong IP); `null` = unverifiable. See [bot detection](/guides/bot-detection). |

### Common reasons

| Reason | Meaning |
| --- | --- |
| `automation_detected` | A webdriver or headless automation tell was present. |
| `ua_bot` | The user agent classified as a bot. |
| `bot:ai_crawler` | A declared AI agent (GPTBot, ClaudeBot, PerplexityBot, Bytespider). |
| `bot:search_crawler` | A search crawler (Googlebot, Bingbot). |
| `bot:id:<id>` | A bot was identified from the registry, e.g. `bot:id:googlebot`. |
| `bot:verified` | The bot is IP-verified — its user agent and IP both check out. |
| `bot:spoofed:<id>` | The user agent claims a bot, but the IP is out of the operator's published ranges. |
| `client_token_missing` | The signed handshake token was absent — the beacon could not confirm a real browser ran. |
| `ip_datacenter` | The IP belongs to a hosting or datacenter range. |
| `ip_vpn` / `ip_proxy` | The IP is an anonymizing VPN or proxy. |
| `ip_scanner` | The IP is a known scanner or abuse source. |

## Bot detection

FormShield names the bot behind each hit and, for operators that publish their IP
ranges (Google, Microsoft, OpenAI, DuckDuckGo), verifies that the request really
came from them. A forged crawler — a user agent claiming to be Googlebot from an
unrelated IP — is flagged as spoofed and scored high. You can also allow or block
bots per project from the dashboard. See [bot detection](/guides/bot-detection) for
the full reference and the allow/block controls.

## Where to view observations

Open the project's **Logs** in the [dashboard](https://formshield.dev/app). Each observation shows its score, decision, reasons, IP profile, and user-agent classification. Filter by decision, action, or IP, and click any IP to see its full profile across observations.

## Single-page apps

In an SPA the beacon records a pageview on the initial document load. Route changes that do not reload the document are not yet tracked as separate pageviews — that is a follow-up. See the [React](/guides/react) and [TanStack Start](/guides/tanstack-start) guides for SPA-specific placement.

## Next steps

<CardGroup cols={2}>
  <Card title="Server reporting" icon="server" href="/guides/server-reporting">
    Capture crawlers and AI bots that never run the beacon.
  </Card>
  <Card title="Framework guides" icon="layers" href="/guides/nextjs">
    Place the beacon correctly in Next.js, React, or TanStack Start.
  </Card>
</CardGroup>
