@sightspool/sdk

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

Install

Script tag (no build step)

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 / bundler

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).

Keys & environments

Your key is publishable — safe to ship in client JS (the Stripe pk_ model). Two prefixes, one per environment:

prefixuse 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 (see captureOnLocalhost), so even a pk_live_ key won't capture from npm 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_…

Frameworks

Next.js (App Router) — next/script

The 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-key is inlined at build time, so NEXT_PUBLIC_SIGHTSPOOL_KEY must be set where Next builds. strategy="afterInteractive" keeps it off the critical path.

React / Next.js — @sightspool/react

For 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.

Content-Security-Policy

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.

Configuration

init(config) — all optional except key. Script-tag installs use the data-sightspool-* attribute equivalents.

optiondefaultpurpose
keyrequired. Publishable key (pk_test_… / pk_live_…).
endpointbundle origin / app.sightspool.comIngest base URL. The <script> install auto-resolves it to where sdk.global.js was served from.
boundaryAsktrueShow the one-tap "did you do what you came to do?" ask at session boundaries.
consenttrueStart 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).
captureOnLocalhostfalseBy default the SDK no-ops on localhost. Set true to capture locally.
debugfalseLog 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.

What it captures

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

Privacy-conscious by default — on without any config:

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.

API

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

Verify the install

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.