capless
Docs Web SDK

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
replaySampleRate0–1, default 1Fraction of sessions recorded.
allowedOriginsoptional, 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 typeWhenNotable fields
pageviewload, pushState/replaceState, back/forwardtitle, referrer, referrerHost, navigation, ttfbMs, loadMs (when already known: the load, else DOMContentLoaded, time; often absent because the tag runs before either)
pageleavenavigating away / tab closingvisibleMs, scrollDepthPct, loadMs (the page's full load time, on the first pageleave after a page load), vitals (lcpMs, fcpMs, cls, inpMs)
clickany clickelement (tag, id, classes, text, href, selector, name from data-capless), x, y, rage
submitform submission (never field values)form (id, name, action, method)
erroruncaught errors and unhandled rejectionserror (kind, message, stack, file, line, column)
hiddentab hidden
customcapless("track", …)name, properties
identifycapless("identify", …)traits
replay_starta 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 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 with sessionId, 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 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.
<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

This page as Markdown

← MCP server · Streams & cursors →