In-product capture for Sightspool. One script tag (or npm package) captures, at the moment of friction, what a user was trying to do (intent), how hard it was (effort), and the account behind it — emitting one linked Signal to your workspace.
v0.1, collect-side. The SDK senses — it captures and analyses; it does not act on your surface (interventions are human-gated, a later wave). Published to npm with SLSA provenance ("Built and signed on GitHub Actions"), built in this public repo.
Install Keys Frameworks CSP Configuration What it captures Privacy API Verify the install
One tag — it auto-inits from its data-sightspool-key:
<script
async
src="https://app.sightspool.com/sdk.global.js"
data-sightspool-key="pk_live_…"
></script>
Then identify the user once known (guard with && until the script has loaded):
window.Sightspool && window.Sightspool.identify(userId, { account, plan })
npm install @sightspool/sdk
import Sightspool from '@sightspool/sdk'
Sightspool.init({ key: process.env.NEXT_PUBLIC_SIGHTSPOOL_KEY })
Sightspool.identify(userId, { account, plan })
init boots passive capture immediately. identify attaches the
user to an account + plan — the only required wiring,
and what lets Sightspool rank by customer (and, with a billing source, by MRR).
Your key is publishable — safe to ship in client JS (the Stripe
pk_ model). Two prefixes, one per environment:
| prefix | use it for |
|---|---|
pk_test_… | development / staging / preview deploys |
pk_live_… | production |
The SDK treats both prefixes identically — there's no client-side special-casing; the prefix tells Sightspool (at ingest) which environment a Signal belongs to, so test traffic never mixes into production analytics. Issue both from Connections → In-product SDK.
The SDK also no-ops on localhost by default (seecaptureOnLocalhost), so even apk_live_key won't capture fromnpm run dev. Test keys are for deployed non-prod environments.
Keep the key in a client-exposed env var (NEXT_PUBLIC_ / VITE_ / PUBLIC_ …) and select test vs live per environment:
# .env.development / preview
NEXT_PUBLIC_SIGHTSPOOL_KEY=pk_test_…
# .env.production
NEXT_PUBLIC_SIGHTSPOOL_KEY=pk_live_…
next/scriptThe idiomatic install is next/script, not a raw <script>. Add it once in your root layout:
// app/layout.tsx
import Script from 'next/script'
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Script
src="https://app.sightspool.com/sdk.global.js"
data-sightspool-key={process.env.NEXT_PUBLIC_SIGHTSPOOL_KEY}
strategy="afterInteractive"
/>
</body>
</html>
)
}
Then identify the user once known (a client component after auth):
'use client'
useEffect(() => {
window.Sightspool?.identify(user.id, { account: user.account, plan: user.plan })
}, [user])
data-sightspool-keyis inlined at build time, soNEXT_PUBLIC_SIGHTSPOOL_KEYmust be set where Next builds.strategy="afterInteractive"keeps it off the critical path.
@sightspool/reactFor React apps, use the official bindings — a declarative provider over the core SDK:
npm install @sightspool/react @sightspool/sdk react
import { SightspoolProvider } from '@sightspool/react'
<SightspoolProvider
apiKey={process.env.NEXT_PUBLIC_SIGHTSPOOL_KEY}
identity={user && { userId: user.id, account: user.account, plan: user.plan }}
>
<App />
</SightspoolProvider>
It ships a "use client" banner (drops straight into a Next.js App Router
server layout), re-fires identify whenever
identity changes, and takes a reactive consent prop for cookie
banners. Full API:
@sightspool/react.
@sightspool/sdk and react are peer dependencies.
Allow the SDK's two footprints — the script load and the ingest beacon. With the standard install they're the same host (the bundle is served from the app it ingests to), so it's one host in two directives:
Script-tag install
script-src https://app.sightspool.com;
connect-src https://app.sightspool.com;
npm / bundler install — the SDK is bundled into your own first-party JS, so no script-src host is needed; only the ingest origin:
connect-src https://app.sightspool.com;
If you pass a custom endpoint, use that origin in connect-src.
The beacon goes via navigator.sendBeacon with a fetch(keepalive)
fallback — both governed by connect-src.
strict-dynamic / nonce. Under
script-src 'strict-dynamic', host allowlists are ignored for scripts —
give the <script> tag your per-request nonce
(nonce={nonce} in Next). connect-src still needs the ingest host.
<style>. Under a strict
style-src without 'unsafe-inline', those styles may not apply
— the prompt stays functional but unstyled (the SDK never throws into
your page). Add 'unsafe-inline' to style-src to style it.
init(config) — all optional except key. Script-tag installs use the data-sightspool-* attribute equivalents.
| option | default | purpose |
|---|---|---|
key | — | required. Publishable key (pk_test_… / pk_live_…). |
endpoint | bundle origin / app.sightspool.com | Ingest base URL. The <script> install auto-resolves it to where sdk.global.js was served from. |
boundaryAsk | true | Show the one-tap "did you do what you came to do?" ask at session boundaries. |
consent | true | Start capturing immediately. false → stay paused until Sightspool.consent(true). |
redact | [] | CSS selectors whose captured text is masked (‹redacted›) before egress. The event is still recorded. |
block | [] | CSS selectors whose events are dropped entirely (same as data-sightspool-ignore). |
captureOnLocalhost | false | By default the SDK no-ops on localhost. Set true to capture locally. |
debug | false | Log every capture decision to the console ([sightspool] …). |
Script-tag attributes: data-sightspool-key,
data-sightspool-endpoint, data-sightspool-redact,
data-sightspool-block (comma-separated selector lists),
data-sightspool-capture-localhost, data-sightspool-debug.
Routes/screen sequence, clicks, dead-clicks and rage-clicks, JS errors and failed requests, and debounced search / filter / command-palette input values (stated intent). At a friction moment or session boundary it may show one rate-limited, fatigue-aware prompt. Each capture emits one Signal (intent + path + account + effort); intent and effort are constructed server-side with calibrated confidence — the SDK ships the raw trace, it never guesses.
Privacy-conscious by default — on without any config:
‹email›, long digit runs → ‹num›. Password / email / tel / credit-card inputs are never captured.localStorage, no raw keystrokes. Debounced search-input values, not a keylog.captureOnLocalhost to override).You control the rest: data-sightspool-ignore or a block selector drops a subtree; a redact selector masks a field's text; Sightspool.consent(false) gates everything until your cookie banner says otherwise.
Sightspool.init(config): void
Sightspool.identify(userId, { account?, plan? }): void
Sightspool.consent(granted: boolean): void // runtime gate; wire to your cookie banner
Sightspool.start(): void // begin capture if init'd with { consent: false }
Sightspool.stop(): void // pause capture and flush
Add data-sightspool-debug (or init({ debug: true })) to log every
capture decision to the console as [sightspool] …. In your Sightspool
workspace, the Connections → In-product SDK card shows raw Signals landing
within seconds of the first beacon (pre-inference), and the /signals page
shows the inferred inbox. Remember capture is suppressed on localhost unless you set
captureOnLocalhost.