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-key="cs_…"></script>
Put it before </head> on every page. Get one from capless.sh or with curl -s https://analytics.capless.sh/new (Quickstart); 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:
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.
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 (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 keycs_…)anonymousId(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)
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 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, 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, 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