# 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-site="cs_…"></script>
```

Put it before `</head>` on every page. The site key (`cs_…`) is public and write-only.

## Sites

A project can have several sites (one per domain or app). Sites are managed with the project API key:

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

# Create
curl -s https://api.capless.sh/v1/web/sites -H "$H" \
  -d '{"name":"shop","allowedOrigins":["https://shop.example"],"replaySampleRate":1}'
# → {"key":"cs_…","name":"shop","allowedOrigins":["https://shop.example"],"replaySampleRate":1,"snippet":"<script async …>", …}

# List, update, delete
curl -s https://api.capless.sh/v1/web/sites -H "$H"
curl -s -X PATCH https://api.capless.sh/v1/web/sites/cs_… -H "$H" -d '{"replaySampleRate":0.25}'
curl -s -X DELETE https://api.capless.sh/v1/web/sites/cs_… -H "$H"
```

| Field | | |
|---|---|---|
| `name` | required | |
| `allowedOrigins` | optional | Restricts which origins may send to the site key, e.g. `["https://shop.example"]`. |
| `replaySampleRate` | 0–1, default `1` | Fraction of sessions recorded. |

## 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` |
| `pageleave` | navigating away / tab closing | `visibleMs`, `scrollDepthPct`, `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`
* `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)

## Streams

Each project's sites write to two [streams](https://capless.sh/docs/streams.md), created on first use:

* `web.events`: one record per event (above). Query it with [`/read` + CEL](https://capless.sh/docs/reading.md), or [materialize it into ClickHouse](https://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://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
```
