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.
<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:
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
// 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:
<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),siteanonymousId(per browser, localStorage),sessionId(30 minutes of inactivity ends a session, shared across tabs),tabId,userIdurl,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, andipHash(keyed hash; raw IPs are never stored)
Streams
Each project's sites write to two streams, created on first use:
web.events: one record per event (above). Query it with/read+ CEL, or materialize it into ClickHouse.web.replay: session replay chunks withsessionId,tabId,seq,part,startedAt,url,fromMs,toMs,frames.
# 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 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 withdata-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.
<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:
script-src https://c.capless.sh; connect-src https://c.capless.sh