# Web SDK

One tag. 2.0 KB (brotli) for analytics; session replay is a separate 1.7 KB chunk that is only downloaded for sampled sessions. No cookies, no dependencies.

```html
<script async src="https://c.capless.sh/c.js" data-key="cs_…"></script>
```

Put it before `</head>` on every page. Get one from [capless.sh](https://analytics.capless.sh/#install) or with `curl -s https://analytics.capless.sh/new` ([Quickstart](https://analytics.capless.sh/docs/quickstart.md)); a project's snippet is also in the dashboard and in `GET /v1/web`.

The key (`cs_…`) is public and can only write events. Each project has one key, and one snippet works on any number of domains. The project's sites are the hostnames that have sent events; they appear in `hosts` without any setup.

## Settings

Read the snippet, the settings and the hosts with the project API key, a dashboard session, or the claim secret of an unclaimed project:

```sh
H="authorization: Bearer $CAPLESS_API_KEY"

curl -s https://api.capless.sh/v1/web -H "$H"
# → {"key":"cs_…","snippet":"<script async …>","replaySampleRate":1,"allowedOrigins":[],
#    "hosts":[{"host":"shop.example","firstSeen":"…","lastSeen":"…","events":1520}]}

# Changing settings needs an API key or an owner or admin session
curl -s -X PATCH https://api.capless.sh/v1/web -H "$H" -d '{"replaySampleRate":0.25}'
```

| Field | | |
|---|---|---|
| `replaySampleRate` | 0–1, default `1` | Fraction of sessions recorded. |
| `allowedOrigins` | optional, default `[]` (any origin) | Origins whose pages may send events, e.g. `["https://shop.example", "*.shop.example"]`. |

`allowedOrigins` makes browsers on other sites that copied your snippet get `403 origin_not_allowed`. It is a deterrent, not security: anyone can send events with their own HTTP client and any `Origin` header. To bound what such traffic can cost you, set a [spend cap](https://analytics.capless.sh/docs/limits.md#spend-cap).

## JavaScript API

```js
// Optional stub so calls made before the script loads are queued:
window.capless = window.capless || function () { (capless.q = capless.q || []).push(arguments) };

capless("track", "checkout_started", { plan: "pro" }); // custom event
capless("identify", "user_42", { email: "jane@example.com" }); // attach a user id (+ traits)
capless("reset");   // forget the user id (logout)
capless("optout");  // stop collecting on this browser; capless("optin") to resume
capless("page");    // record a pageview manually (SPA routing is detected automatically)
```

Name important elements for click analytics with `data-capless`:

```html
<button data-capless="upgrade-cta">Upgrade</button>
```

## What is collected automatically

| Event `type` | When | Notable fields |
|---|---|---|
| `pageview` | load, `pushState`/`replaceState`, back/forward | `title`, `referrer`, `referrerHost`, `navigation`, `ttfbMs`, `loadMs` (when already known: the load, else DOMContentLoaded, time; often absent because the tag runs before either) |
| `pageleave` | navigating away / tab closing | `visibleMs`, `scrollDepthPct`, `loadMs` (the page's full load time, on the first `pageleave` after a page load), `vitals` (`lcpMs`, `fcpMs`, `cls`, `inpMs`) |
| `click` | any click | `element` (`tag`, `id`, `classes`, `text`, `href`, `selector`, `name` from `data-capless`), `x`, `y`, `rage` |
| `submit` | form submission (never field values) | `form` (`id`, `name`, `action`, `method`) |
| `error` | uncaught errors and unhandled rejections | `error` (`kind`, `message`, `stack`, `file`, `line`, `column`) |
| `hidden` | tab hidden | |
| `custom` | `capless("track", …)` | `name`, `properties` |
| `identify` | `capless("identify", …)` | `traits` |
| `replay_start` | a recording starts on a page load | |

Every event also carries:

* `timestamp` (client), `receivedAt` (server), `site` (the key `cs_…`)
* `anonymousId` (per browser, localStorage), `sessionId` (30 minutes of inactivity ends a session, shared across tabs), `tabId`, `userId`
* `url`, `host`, `path`, `query`, `hash`, `utm` (`source`, `medium`, `campaign`, `term`, `content`), `clickIds` (`gclid`, `fbclid`, …)
* `device` (`browser`, `browserVersion`, `os`, `deviceType`, `userAgent`, `screen`, `viewport`, `pixelRatio`, `language`, `timezone`)
* `geo` (`country`, `region`, `city`, `postalCode`, `continent`, `timezone`, `latitude`, `longitude`, `asn`, `asOrganization`)
* `isBot`, and `ipHash` (keyed hash; raw IPs are never stored)

## Delivery

Browser events are acknowledged once the collector has buffered them, and committed to the stream in batches. A quiet site's requests are saved durably before the acknowledgement and committed within about 30 seconds. A busy site (several requests per second) is buffered in memory and committed about once per second; a collector crash can lose up to about a second of those events. Session replay chunks are sent every 15 seconds and when the tab is hidden or closed. The authenticated [ingestion API](https://analytics.capless.sh/docs/streams.md) is unaffected: it returns only after a batch is durably committed.

The collector answers `402` with the reason in `error.code` (for example `event_limit_reached` or `spend_cap_reached`) while collection is paused; those events are not stored. The first event of a new key creates its project; new projects are rate limited per IP address (`429 rate_limited`).

## Streams

The snippet writes to two built-in [streams](https://analytics.capless.sh/docs/streams.md), created with the project. Built-in stream names start with `$`; they cannot be renamed or deleted (`403 built_in_stream`).

* `$web.events`: one record per event (above). Query it with [`/read` + CEL](https://analytics.capless.sh/docs/reading.md), or [materialize it into ClickHouse](https://analytics.capless.sh/docs/materializations.md).
* `$web.replay`: session replay chunks with `sessionId`, `tabId`, `seq`, `part`, `startedAt`, `url`, `fromMs`, `toMs`, `frames`.

```sh
# Rage clicks and what was clicked
curl -s 'https://api.capless.sh/v1/streams/$web.events/read' -H "$H" \
  -d '{"filter":"entry.type == \"click\" && has(entry.rage)","map":"{\"path\": entry.path, \"element\": entry.element}"}'
```

[Erasures](https://analytics.capless.sh/docs/erasures.md) work on both streams, e.g. `entry.userId == 'user_42'`.

## Session replay privacy

Private by default:

* Input values are masked (`*`); password fields are never recorded. Opt specific fields in with `data-capless-unmask`.
* Text inside `.capless-mask` / `[data-capless-mask]` is masked.
* `.capless-block` / `[data-capless-block]` subtrees are replaced by an empty box.
* Script contents and inline event handlers are never recorded.

```html
<div class="capless-mask">Jane Doe · jane@example.com</div>
<iframe data-capless-block src="…"></iframe>
<input name="coupon" data-capless-unmask>
```

## Content Security Policy

If your site sends a CSP, allow the script and its collector:

```text
script-src https://c.capless.sh; connect-src https://c.capless.sh
```
