Docs / Developer Reference / Documentation

Spectra Developer Documentation

Spectra is interface intelligence for the web. It checks every visit in your users' own browsers, finds the controls they cannot use and the errors they hit, names the release that caused them, and keeps the replay of the visits where something went wrong, so you can watch exactly what happened. Recording runs in the background. The checks it makes on your page wait for a moment nobody is using it, and the recorder's size is held to a budget on every release.

60-Second Quickstart

Add the snippet from your Sites page to the head of every page you want recorded. It carries your key, your site id and the address recordings are sent to; the exact values are shown when a key is issued. The recorder starts as soon as the script arrives. Anything your page needs to tell it before then can wait in window.spectraQ, as described below.

HTML / Universal Snippet
<script src="https://api.spectra-trace.com/v1/s.js" data-api-key="sp_live_xxxxxxxx" data-site-id="site_xxxxxxxx" data-endpoint="https://api.spectra-trace.com/v1/ingest" async></script> <!-- Or programmatic initialization: --> <script src="https://api.spectra-trace.com/v1/s.js"></script> <script> Spectra.record({ apiKey: 'sp_live_xxxxxxxx', siteId: 'site_xxxxxxxx', endpoint: 'https://api.spectra-trace.com/v1/ingest' }); </script>

The tag is read for these attributes and no others. A misspelled one is ignored in silence, so if the recorder is not doing what you expect, check the spelling here first.

Attribute Required What it does
data-api-key Yes The key issued for this site. Without it the recorder does not start at all.
data-site-id Yes Which site these recordings belong to. Left out, the key is used as the site id and the sessions are filed under a site that does not exist.
data-endpoint Yes Where recordings are sent. Left out, the recorder guesses the origin the script came from, which is only right while the bundle is served from the ingest host.
data-tunnel No The prefix on your own domain that forwards to Spectra. Set this instead of the endpoint for a first-party install. See Serving Spectra from your own domain.
data-journey-origins No The other origins this visitor's journey crosses, comma separated, for example https://app.example.com. Without it a visit that moves from your marketing site to your application becomes two unrelated recordings. Name them on both sides. See Recording a whole journey.
data-tracing No Present, with no value needed, sends W3C trace headers with requests to this page's own address, so a replay and your server logs name the same request. See Linking a replay to your server logs.
data-tracing-origins No Other addresses that may carry those headers, comma separated, for example your API on https://api.example.com. Those servers must allow traceparent and baggage first, or the requests fail. Naming any address turns tracing on.
data-trace-sample-rate No What fraction of traces your backend is asked to keep, 0 to 1. Defaults to all of them. Turn it down on a busy site if your tracing tool bills by volume; log correlation keeps working at any rate.
data-sample-rate No What fraction of visitors to record, 0 to 1. Records all of them by default. The answer comes from the visitor rather than from a coin toss, so you get that fraction of your visitors with whole journeys, not that fraction of your page views with holes in them, and a visit already under way is always finished. Most sites should leave this alone: an uneventful visit already costs almost nothing, and sampling throws away the visits that turn out to matter.
data-environment No Which deployment this is, for example production or staging. Carried with every recording so a staging session cannot be mistaken for a customer's.
data-block-all-media No Present, with no value needed, replaces every image, video and canvas with a placeholder of the same size. For a site where the pictures themselves are the confidential part and a missed selector would be an incident rather than a gap. The layout is unchanged, so the replay is the real page with the pictures removed. Images referenced from CSS backgrounds are not replaced, but none of them is kept with a recording: a replay shows them only for as long as your site still serves them.
data-keyboard No false stops recording the keys that are decisions: Enter, Escape, Tab, the arrows, Backspace, and combinations such as Ctrl+Z. On by default, because without them a replay cannot tell somebody submitting a form from somebody giving up on it, and a menu driven by the keyboard looks like nothing is happening. What a visitor types is never recorded this way on any setting: characters reach a recording only as a field's value, under the masking rules, and a combination pressed in a masked field has its letter replaced too.
data-long-tasks No false stops marking the moments the page stopped responding. On by default. Without them a frozen replay is indistinguishable from a visitor who simply stopped moving. Reported by Chrome and Edge; Safari and Firefox do not measure it, and a recording from those browsers has no such marks.
data-console-levels No Which console levels to record, comma separated, for example debug,log,warn,error. The default is warnings and errors, which is what most sites want: on a busy page the other levels are a running commentary that fills a recording without explaining anything. Uncaught errors and unhandled promise rejections are always recorded whatever you choose.
data-network-bodies No The addresses whose request and response bodies a replay may keep, comma separated, for example /api/,/graphql. Every request is already recorded with its method, address, status, size and timing; this adds the part that says why it failed. Nothing is kept for an address you do not name here. What is kept is redacted first: a value under a key such as password, token or cardNumber is replaced before anything reads it, every remaining piece of text goes through the same scrubbing your page content does, a body that is not text is recorded as its size and type, and there is a limit on each body and on the visit as a whole.
data-network-headers No Which request and response headers may be kept, comma separated, for example content-type,x-request-id. Nothing is kept unless you name it. Authorization, Cookie and the other credential headers are refused even when named, and a header whose own name says it carries a secret is replaced rather than kept.
data-network-deny-keys No Extra field names to treat as secrets, comma separated. The built-in list covers the usual ones; use this for the fields only you would recognise, for example an internal reference that identifies a person.
data-network-allow-keys No Field names you know are not secrets, comma separated, kept although a built-in rule would hide them: a short name such as key, pin or card, or one that ends in it, such as issueKey. Exact names only. A name with a secret in it, such as apiKey, password or token, stays hidden however it is listed, so does anything in your deny list, and the values are still checked for card numbers and email addresses.
data-network-sockets No false stops recording WebSocket, server-sent event and beacon activity. On by default, and it records only the connection and how much went each way, never what was said over it. Without it a replay of a page that does its work over a socket shows an empty network list under a screen that is plainly updating.
data-release No The build this page came from, for example 2026.09.11. Sessions and issues are grouped by it, which is how you tell whether a problem arrived with a deploy, and the Integrity page says how each broken control stands in your newest release against the others: new in it, gone from it, or more or less often. This is the one place a templating layer can write it for a script-tag install.
data-context No The state this page is in, read from the page itself, for an install with no code to describe it, for example theme: html@data-theme; member: body.logged-in. Each label is a name, a colon and where to find it, separated by semicolons: a selector followed by @ and an attribute gives that attribute's value, and a selector on its own gives yes or no. The Integrity page uses it to say where a broken control happens.
data-consent No true or false. Say false when your consent banner has not been answered yet, and nothing is recorded until it is. Leave the attribute off entirely and the recorder behaves as it always has, so adding it is a decision rather than a default. To answer later, from the banner itself, call spectraQ.push(['consent', true]).
data-capture-before-consent No Reproduce bundle only (/v1/sr.js), and only with data-consent="false". Present, the visit is recorded from the page's load while your banner is unanswered, but held in the page's memory: nothing is written to the visitor's device and nothing is sent to Spectra until they accept. Accepted, it is sent from the start, so the test begins where the visit began. Refused, or left unanswered when the tab closes, it is dropped and never leaves the browser. Whether recording before consent is lawful for your site is your decision, per region; it is off unless you add this.
data-inputs-as-typed No Reproduce bundle only (/v1/sr.js). There, what visitors type is recorded as typed, emails and phone numbers included, so a replay can sign in or search the way the visitor did. Say data-inputs-as-typed="false" to keep typed text scrubbed of personal details instead. Passwords, every card field and one-time codes are never recorded, only their shape, whatever this says. The base script has no such setting.
data-reproduce-on-failure No Reproduce bundle only (/v1/sr.js). There, each page is held in the visitor's browser from its load and sent only when something goes wrong in it, and healthy visits are sent whole as your monthly Reproduce budget allows. Say data-reproduce-on-failure="false" to record every visit whole instead, for a test environment or a small internal app. The base script has no such setting.
data-respect-dnt No Present, with no value needed, honours Do Not Track and Global Privacy Control and records nothing for visitors who send either. Off by default on purpose: Brave sends GPC on every request and Firefox private windows send DNT, so switching this on will stop recording a real share of your visitors. That is the right trade for some sites and the wrong one for others, which is why it is yours to make.
data-persistence No local (the default), session, or memory. How long anything the recorder writes may stay on a visitor's device. Consent is required for storing something on a device, not for recording a visit, so session lets you keep recording when your banner has been refused. It keeps nothing past the visit: the visitor id moves to sessionStorage and undelivered data is held in the page. Sessions still join across the pages of a visit, journeys still work, and a refused upload is still retried while the page is open. What you give up is returning visitors, who arrive as new ones, and an upload still waiting when the tab closes, which is lost rather than sent later. memory writes nothing at all and every page becomes its own recording, which also means every page counts as a separate session against your plan. That makes session the one to reach for unless you have a reason not to. Cross-domain journeys are unaffected by any of these, because they travel in the link rather than in storage.
data-ignore-bots No Present, with no value needed, records automated browsers too. The recorder refuses them by default, which is almost always what you want; turn this on when you are testing your own install with a headless browser and wondering why nothing appears.
data-debug No Present, with no value needed, makes the recorder narrate what it is doing to the browser console. Use it while installing, then take it off. It is the only way a script-tag install can ask the recorder what it decided.
Async Load Queue: For Google Tag Manager or custom script loaders, commands can be pushed to window.spectraQ prior to script execution without risking execution order errors.

After the snippet: what to set up

The snippet on its own records visits, finds the moments people struggled, and keeps the sessions worth keeping. Everything below is optional, and each one answers a question you will eventually ask of a recording. They are in the order most teams want them.

1. Say who the visitor is

Without this every session belongs to an anonymous visitor, and a support request that starts with a customer name has nowhere to go. One call, at sign-in:

Spectra.current()?.identify(user.id);

It applies to the whole session including everything before the call, so signing in halfway through a visit still attaches the part that led up to it. Their previous visits group under the same person too. In the session list, clicking a visitor shows everything else they did.

2. Say which build and which deployment

Two attributes your build writes, and the reason to bother with them is the question "did this start with the last deploy", which nothing else can answer.

Script tag
<script src="…/v1/s.js" data-api-key="…"
        data-release="2026.09.25"
        data-environment="production"></script>

The Issues page then groups problems by build and tells you which one a problem arrived in and whether it is still there. The environment keeps your staging visits from being read as customers: set it on every deployment, not only production, or the untagged ones are the ambiguous ones.

3. Say what state the page is in

One address is often many pages: signed in or out, a plan, an experiment, a feature you are trying out. Say which one this is, and the Integrity page tells you where a broken control happens, for example "when plan is free and on screens 360px and narrower", instead of only that it happens somewhere.

JavaScript
Spectra.record({
  apiKey: '…',
  integrity: {
    context: () => ({ plan: user.plan, checkout_test: variant }),
  },
});

// When the state changes without a page load:
Spectra.current()?.setContext({ plan: 'pro' });
Script tag
<script src="…/v1/s.js" data-api-key="…"
        data-context="theme: html@data-theme; member: body.logged-in"></script>

Up to ten labels, each a short state that many visitors share. A value that looks like an email address, an account number or a token is refused. Whether the visitor is signed in, their screen size, browser, language and light or dark mode are known without asking, and the build too when you set it above. Passing null to setContext removes a label.

Experiments are read without asking from Optimizely Web Experimentation and PostHog when they are on the page, in the room your own labels leave. For any other testing tool, pass the arm where the tool reports it. The name can be the tool's own; it is made into a label for you.

GrowthBook
new GrowthBook({
  trackingCallback: (experiment, result) => {
    Spectra.current()?.setExperiment(experiment.key, result.key);
  },
});

A button that reads the same in two layouts of a page is counted as one, as long as the page never shows both at once. A button with no text, or one of several that read the same, is known by where it sits, so in another layout it can be counted as two. To keep it one button in every version of a page, give it a name: data-spectra-id="checkout".

Some faults are what you meant: an export button behind a paywall on the free plan, a link hidden from people who already pay. Mark one as intended on the Integrity screen, everywhere or only in the state it was found in, and it leaves the list and the score. In any other state the control is still checked. The same screen sets how many visits a fault must have met before it is listed.

4. Report the errors your own code catches

Uncaught errors are recorded for you. An error your framework catches never reaches the browser, so nothing outside your code can see it, and the better your error handling the less a replay knows. One line puts them back. See Reporting errors your code catches for React, Vue, Angular and script-tag examples.

5. Say where to be told when something breaks

Under each site, in Tell me when it breaks, paste a Slack incoming webhook or any address of your own. Without it, a page that stops working is found immediately and mentioned whenever somebody next opens the dashboard, which at three in the morning is not much use. See Being told when something breaks.

6. Decide what a replay may keep of your traffic

Every request your page makes is already recorded with its address, status, how long it took and what asked for it, and so is everything the page loaded. What is not kept, until you name the addresses, is what the requests actually said:

networkCapture: {
  bodies: { urls: ['/api/'] },
  headers: ['content-type', 'x-request-id'],
}

That is the difference between "it returned 400" and "it returned 400 because the postcode was empty". Nothing is kept for an address you did not name, credential headers are refused even if you list them, and values under names like password or cardNumber are replaced before anything reads them. Start with one address and widen it.

7. Mark anything that must never be recorded

Input values, and what a visitor types into an editable region such as a chat box or a rich-text editor, are masked by default. Passwords, card fields and one-time codes always are, whatever else you set. For a region of the page, add data-spectra-block to the element and it is replaced by a placeholder of the same size. For a screen you cannot mark up in advance, such as a document another customer uploaded, pause instead:

Spectra.current()?.pause();   // before showing it
Spectra.current()?.resume();  // after

The session carries on either way. If the pictures themselves are the confidential part and a missed selector would be a problem rather than a gap, use data-block-all-media and every image, video and canvas becomes a placeholder. See On-Device Privacy and PII Masking.

What you do not have to do

Page loads, scripts, stylesheets, fonts and images are recorded without any configuration, as are clicks, typing, scrolling, the keys that are decisions such as Enter and Escape, how fast each page was for that visitor, and the moments the page stopped responding. Rage clicks, dead clicks, form trouble, failed requests and stuck loading screens are detected for you. None of it needs an option set.

Verifying Installation

Confirm the install from your browser's developer tools console. Run the following command on any page where the recorder is installed:

Browser Console Verification
Spectra.current().flag('test_install');

Within a few seconds the Replay screen lists this visit, kept for the reason Install test.

Spectra is the name the script tag gives the recorder. If you installed the npm package instead, import current from it and call current()?.flag('test_install') from your own code; the same goes for every Spectra.current() on this page.

Serving Spectra from your own domain

Content blockers refuse requests to third-party hosts that appear on a filter list. Served from Spectra's host, the recorder is third-party to your site. Served from your own domain, there is nothing to refuse: the script, its configuration and every recording travel on your origin under a path that says nothing about what it is. This is the install the Sites page recommends, and it needs one forwarding rule on your host.

Your own Content-Security-Policy is the second reason, and on measurement it is the larger one. We ran the recorder against forty public sites: seventeen of them refused the direct install at connect-src before it could fetch its configuration, so it started, threw nothing, and recorded nothing. Nothing on your page would have told you. Served from your own origin the recorder is same-origin, which connect-src 'self' already permits, so a policy you have not changed keeps working.

If you would rather install directly and your site sets a policy, allow https://api.spectra-trace.com in both script-src and connect-src. The recorder now says so in your browser console when a policy blocks it, once per page, naming the directive that refused it.

How it works

Your host forwards one prefix, for example /q4hx/*, to https://api.spectra-trace.com/v1/*. The Sites page shows the tag with your key and a prefix chosen for your site; any prefix works as long as the tag and the rule agree.

Every rule below forwards the whole prefix, which is what we recommend. If your host or firewall needs the paths listed instead, these are all of them. A path your proxy does not forward is lost with no error on your page and nothing on ours, so the list has to be complete:

Path What it carries
/s.jsThe recorder itself.
/iRecordings.
/cThis site's configuration.
/pOne small anonymous count per page.
/uThe per-visit analytics summary.
/usThe page snapshot a heatmap is drawn on.
/aStylesheets, stored once per version and shared by every recording that uses them.
/mImages and fonts, so a replay looks like the page did.

The tag
<script src="/q4hx/s.js" data-api-key="sp_live_xxxxxxxx" data-site-id="site_xxxxxxxx" data-tunnel="/q4hx" async></script>

The rule, per host

Cloudflare Pages (a Pages Function)
// functions/q4hx/[[path]].js export async function onRequest({ request, params }) { const path = [].concat(params.path ?? []).join('/'); const { search } = new URL(request.url); return fetch(new Request(`https://api.spectra-trace.com/v1/${path}${search}`, request)); }
Netlify (netlify.toml)
[[redirects]] from = "/q4hx/*" to = "https://api.spectra-trace.com/v1/:splat" status = 200 force = true
Vercel (vercel.json)
{ "rewrites": [{ "source": "/q4hx/:path*", "destination": "https://api.spectra-trace.com/v1/:path*" }] }
nginx
location /q4hx/ { proxy_pass https://api.spectra-trace.com/v1/; proxy_set_header Host api.spectra-trace.com; proxy_ssl_server_name on; client_max_body_size 16m; }
Next.js (next.config.js)
async rewrites() { return [{ source: '/q4hx/:path*', destination: 'https://api.spectra-trace.com/v1/:path*' }]; }
Express, with http-proxy-middleware
// Mount before any body parser. app.use('/q4hx', createProxyMiddleware({ target: 'https://api.spectra-trace.com', changeOrigin: true, pathRewrite: (path, req) => '/v1' + req.originalUrl.slice('/q4hx'.length), }));
Three things the proxy must do, and every rule above does: forward the request headers, because recordings identify themselves with headers beginning X-Spectra- and a proxy that forwards only the content type looks like it works and records nothing; forward the method and body unchanged, which is why the Express rule goes before any body parser; and let a large body through, because the first picture of a busy page arrives in one request, which is why the nginx rule raises its limit.

Recording a whole journey

Do you need this?

If your whole site sits at one address, like example.com, then no. Paste the snippet and you are finished. A visit that moves through ten of your pages is already one recording.

You need this page if your visitors move between two different addresses that both belong to you. A marketing site at www.example.com and an app at app.example.com is the usual shape. Without the setting below, somebody who reads your pricing page and then signs up becomes two separate recordings, and the interesting part is split down the middle.

The rule that matters most

Spectra can only record a page that has the snippet on it. There is no setting that lets one page record another. If a page does not have the snippet, that page produces nothing at all, and nothing anywhere reports it.

So put the snippet on every page you want to watch, at every address. In practice that means putting it in the shared layout or header template all your pages are built from, once, so nobody has to remember. If your two sites are separate projects, each one needs it in its own layout.

The setting below does a different job. It does not reach other pages. It tells two snippets that are already running that they are watching the same visit, so their two recordings become one.

Joining two of your addresses

List your addresses in the tag, and use the same tag on every page of both sites:

The same on every page, on both sites
<script src="https://api.spectra-trace.com/v1/s.js" data-api-key="sp_live_xxxxxxxx" data-site-id="site_xxxxxxxx" data-endpoint="https://api.spectra-trace.com/v1/ingest" data-journey-origins="https://www.example.com, https://app.example.com" async></script>

Two things to get right, and that is all of it:

List every address, including the one the page is on. The simplest thing is to paste the identical tag everywhere rather than tailoring the list per site. An address you have not listed is never given a session and never accepts one, so listing only one side leaves you exactly where you started: two recordings, nothing broken.

Use the same data-site-id on both. A recording is filed under its site and its session together, so two different site ids mean two recordings no matter what else is true.

What this covers

People leave a page in more than one way, and something that only handled links would miss most of them:

How the visitor moves Between your subdomains To another domain of yours
Clicks a link, or submits a form Yes Yes
Your server redirects them, after signing in Yes With one header, below
Types the address, or follows a link in an email Yes No
Your own code sets location.href Yes With one line, below

About the cookie, and visitors who refuse it

Between subdomains of one site, a small cookie carries the session. It is the only thing that survives a redirect made by your own server, because the browser sends it whatever caused the visitor to move.

If you list no addresses, no cookie is written at all. That is the ordinary install, and you can tell a privacy reviewer plainly that the product sets no cookie. When you do list them, the cookie holds a reference to the session and nothing about the person, has no expiry date, and disappears when the browser closes. It is a handover between two pages of one visit, not a record of who somebody is.

If a visitor blocks or refuses cookies, nothing breaks. Moving through one address keeps working, because that never used a cookie. Clicking a link or submitting a form to your other address keeps working, because the session travels on the link itself. Only the two cases that need the cookie fall back, and they fall back to what you had before: two recordings instead of one, with no errors and nothing for the visitor to notice.

This is not the same as consent to record. If a visitor has refused to be recorded, that is the hasConsent option, and it stops recording altogether. This section is only about somebody who allows recording but blocks cookies.

A redirect your own code makes

There is one move nothing can see: your code setting location.href. If it goes to another domain of yours, hand the session over in one line:

Carrying the session by hand
window.location.href = window.Spectra.current().handoff(checkoutUrl);

An address you did not list comes back unchanged, so this is safe to put before any redirect, including one to a payment page. When the destination is somewhere no recording is possible, it also marks the moment in the replay, which is worth doing on its own.

A redirect your server makes, between two of your domains

This is the last case, and the only one that needs anything from your backend. Between subdomains it is already handled and you can skip this. It applies when your server decides to send the visitor from one of your domains to a genuinely different one, for example after a sign-in.

Nothing in the browser can carry that on its own. There is no link to attach anything to, because the move was decided on your server after the page was gone, and a cookie cannot cross from one domain to another. The only thing in a position to carry it is the server issuing the redirect. So give it something to carry:

On the page, when you call your backend
fetch('/login', { method: 'POST', headers: { 'X-Journey-Token': window.Spectra.current().journeyToken() }, body: form, });
On your server, on the redirect it sends back
Location: https://shop.example.com/checkout?_spz=<the value of that header>

The arriving page reads it and carries on as the same recording. The header name is yours to choose; only the _spz parameter matters, and it is removed from the address before the page records anything.

What is in that token. A reference to the session, a counter, a timestamp, and the key that lets the page it opens carry on recording this visit. No API key, no user id, no personal data of any kind. The key reaches this one visit and nothing else, because every visit is given a key of its own. Treat the token as you would a short-lived session link, and keep it out of logs you share. A page stops accepting it after one minute, and only ever at an address you listed. If you pass it somewhere you did not list, nothing happens at all.

Why this one is not automatic, when everything else is: a cross-domain redirect happens entirely on your server. The browser is told where to go and goes, and no page of yours is involved to carry anything. Every session replay product has this limit. Most do not offer a way through it at all.

Sites you do not own

This is the distinction that matters, and it is about whose page it is, not which address it sits at. Your second domain works because you can put the snippet on it. A payment page at Stripe or Dodo, or a sign-in page at an identity provider, is somebody else's site: you cannot put a script there, so nobody can record it. Not us, and not any other session replay product either.

What you get instead is the shape of the absence. The replay shows left for checkout.stripe.com, then returned after 3m 12s, and the visit carries on as the same recording. What happened before the payment and what happened after it stay one story rather than becoming two.

If journeys still split, check these in order. Does every page carry the snippet, including the one the visitor landed on second? Does each site list the other's address as well as its own? Do both use the same site id? If all three are right and only server-side redirects between two different domains are splitting, that is a real limit rather than a mistake: there is no link to carry anything, and a cookie cannot cross domains. Sending the header Referrer-Policy: no-referrer also turns this off between separate domains, because the arriving page is then told nothing about where the visitor came from.

Linking a replay to your server logs

A replay stops at the edge of the browser. It shows you that a request took nine hundred milliseconds and came back a 500; it cannot show you what your server did inside it. So the person debugging has the replay open in one window and their logging or tracing tool in the other, trying to match a timestamp against a list of thousands, for a request that happened four hours ago.

Turn this on and both sides give the request the same name, so you can go straight from one to the other.

Same-origin requests
record({ apiKey: 'sp_live_xxxxxxxx', siteId: 'site_xxxxxxxx', tracing: true, });

Every request your page makes to its own address now carries two extra headers:

Header What it is
traceparent The W3C standard for naming a request across systems. If you run OpenTelemetry, Datadog, Jaeger, Honeycomb or anything else that speaks it, your server already understands this header and will hang its own work underneath it. Nothing to configure on your side.
baggage Carries spectra.session=<id>. If you have no tracing tool at all, log this one value and every line of your server log can say which replay it belongs to.

The replay records the same trace id beside each request, so a viewer has something to paste into your tracing tool.

Requests to a different address

If your API lives somewhere else, name it:

Naming an API on another address
record({ apiKey: 'sp_live_xxxxxxxx', siteId: 'site_xxxxxxxx', tracing: { origins: ['https://api.example.com'] }, });
Your API has to allow the headers. Sending an unexpected header to a different address makes the browser ask permission first, and a server that does not answer correctly fails the whole request. So add traceparent and baggage to your Access-Control-Allow-Headers before you name an address here. This is why it is never done for you: a missing link between two tools costs you a search, and a broken API call costs you an outage.

Requests to your own address are safe without any of that, which is why tracing: true covers them and nothing else.

Keeping your tracing bill and your metrics sane

Two things are worth setting deliberately before you turn this on across a busy site.

How many traces your backend is asked to keep. The traceparent header carries an instruction: keep this one. By default every request carries it, so correlation works without tuning, but that also means a tracing tool that charges by volume will keep everything. If you have sampling turned down, turn this down to match:

Marking one trace in twenty
record({ apiKey: 'sp_live_xxxxxxxx', siteId: 'site_xxxxxxxx', tracing: { sampleRate: 0.05 }, });

Turning it down does not turn off the link. The headers still go out on every request; the trace is simply marked as one your backend may drop. Since logs are usually not sampled at all, correlating a log line with a replay keeps working for every single request, whatever rate you pick.

Where the session id is allowed to end up. The session id is unique per visit, which is exactly what makes it useful for correlation and exactly what makes it dangerous as a metric dimension: a metric split by it becomes one series per visitor, which is how monitoring bills get out of hand.

By default there is nothing to worry about. OpenTelemetry does not copy baggage onto spans unless you have added the baggage span processor yourself, and nothing copies it onto metrics. If you have added that processor, or you generate metrics from span attributes, do one of these:

Keep it off your metrics
# If you use the baggage span processor (Python shown), leave it out: BaggageSpanProcessor(lambda key: key != "spectra.session") # Or, if you build metrics from span attributes, leave it out # of the dimensions. It belongs on traces and logs, not metrics.

On a trace or a log line it costs nothing: both are stored per event and looked up by id, which is the whole point of putting it there. It is only aggregation that cannot take a value unique to every visitor.

On OTLP. Spectra does not send anything over OTLP, and does not need to. OTLP is how spans and metrics are exported to a collector; what makes a replay and a trace line up is propagation, which is the traceparent header and a separate standard. Your existing tracing keeps exporting over OTLP exactly as it does today, and its spans now carry an id the replay also knows. If you want Spectra's own events inside your collector rather than in our dashboard, that is a different thing and we do not do it yet.

Things it will not do

It will not disturb tracing you already have. If your page already sends traceparent, that one is left exactly as it is and we record its id as the one to search for. If you already use baggage, ours is added to yours rather than replacing it.

Nothing is sent unless you ask. With no tracing setting, no request is touched at all.

Spectra is not a tracing tool. We do not collect spans or show you a flame graph, and we are not trying to replace what you already run. All this does is make sure both systems call the request by the same name.

Nothing personal travels in these headers. A trace id, a request id and the session id. No account details, no API key.

What does not work

A DNS alias is not first-party. Pointing a subdomain of yours at Spectra's host with a CNAME does not help: uBlock Origin on Firefox resolves the alias and blocks the request anyway. The forwarding has to happen on your own host.

Some visitors cannot be recorded by anyone. A visitor with JavaScript disabled, or a blocker set to refuse every script, does not run the recorder at all. No install changes that.

Content security policy and bundled apps

If your site sends a Content-Security-Policy, add worker-src blob: whichever install you chose. The recorder compresses recordings on a worker it builds from a blob: URL, and worker-src falls back to script-src when it is absent, so a policy of script-src 'self' refuses it. Nothing breaks: the recorder compresses on your visitor's own thread instead, on the page whose responsiveness you installed us to measure, and says so once in the browser console with the line to add.

Beyond that, a first-party install needs nothing added, because everything else it asks for is 'self'. The direct install also needs https://api.spectra-trace.com in script-src and connect-src.

First-party install
Content-Security-Policy: script-src 'self'; connect-src 'self'; worker-src 'self' blob:;
Direct install
Content-Security-Policy: script-src 'self' https://api.spectra-trace.com; connect-src 'self' https://api.spectra-trace.com; worker-src 'self' blob:;

These are the directives Spectra needs; keep whatever else your policy already sends.

If your policy refuses Spectra's address, the recorder says so once in the browser console, names the directive that refused it, and stops on that page, so it costs your visitors nothing. A policy sent as Content-Security-Policy-Report-Only refuses nothing, and recording carries on as normal.

If you bundle the recorder rather than loading the tag, pass the prefix as the tunnel option and it is used for every request:

Bundled
import { record } from '@spectra-trace/recorder'; record({ apiKey: 'sp_live_xxxxxxxx', siteId: 'site_xxxxxxxx', tunnel: '/q4hx' });

A tunnel value that already carries a query string keeps the older ?target= form, so proxies written against it keep working. New installs should use the prefix: one rule instead of a hand-written proxy, and nothing for a filter to match.

Recording rules: what gets recorded

Spectra does not keep every session. A session is kept when something worth seeing happens: an error, a sign of frustration, or a moment your own rules mark as important. Everything else is discarded, which is why your quota goes a long way.

Automatic Default Triggers

Shown as What happened Code
JS error An uncaught JavaScript error. uncaught_error
Unhandled rejection A promise rejected with nothing to catch it. unhandled_rejection
Console error A console.error() call. console_error
Failed request A request answered with a 4xx or 5xx status. Which ones count is yours to decide: see Choosing which failed requests count, below. network_error
Slow response A request somebody pressed a button for and then waited ten seconds or more to be answered. Only requests that began while somebody was waiting count, so long-polling, uploads and background work are not mistaken for a wait. slow_request
Unreachable request A request that never completed at all: refused by the browser as cross-origin, a connection closed, a name that would not resolve, a certificate rejected, or the network gone. Requests your own code cancelled are not counted, and neither are those cut short by the visitor leaving. network_unreachable
Resource failed A script or stylesheet the page asked for and did not get, which is what a bad deploy looks like from the browser. Images and media that fail are recorded too, and shown on the recording, but do not start one on their own. resource_load_error
Blocked by policy Your Content Security Policy refused something the page tried to load or run. The browser writes this to its own console, where no script can read it, so it is heard from the policy event instead. csp_violation
Rage click Several rapid clicks on the same spot that changed nothing. rage_click
Dead click A click on something that looks interactive and is not. dead_click
Form error A form submitted and answered with a validation error. form_error
Form thrashing A field cleared and retyped repeatedly. form_thrash
Abandoned after effort A visitor left after sustained interaction with no completion. abandoned_after_effort
Stuck loading A page that stayed in a loading state past the threshold. stuck_loading
Struggle A run of the friction signals above in one short window. struggle
Abandoned a blank page The page never drew anything and the visitor left. abandoned_blank_page
Page did not render The page loaded but the things people use on it are not there, which is what a broken route or a failed deep link looks like. ghost_render
Recorded for Integrity A control on the page was covered by something else, hidden, or had no size at that moment, so the visit is kept for the integrity check on the paid plans. It is not reported as a problem on its own: the Integrity page lists a control only once visitors were kept from using it. unusable_control
Install test The check you run from the browser console to confirm the install. test_install

Choosing which failed requests count

Until you say otherwise, every failed request is a problem: any answer from 400 up, and any request that never completed, which a rule names as 0. A request blocked by the browser for being cross-origin (CORS) is one of those: the browser hides its status, so it is a 0 and not a 4xx. Each failed request records the visit and is listed under Issues.

Some failures are expected, such as a 401 from a check for whether the visitor is signed in. A rule leaves those out, or counts one again. Rules are read in order and the last one that matches a request decides it.

Failed request rules
Spectra.record({ apiKey, siteId, endpoint, failedRequests: [ // A sign-in check answers 401 to everybody who is not signed in. { status: [401], urls: ['/api/session'], count: false }, // Missing source maps are nobody's problem. { status: [404], urls: [/\.map$/], count: false }, // An ad blocker's work on somebody else's address. { status: [0], urls: ['ads.example.com'], count: false }, ], });
  • status: codes such as 404, '4xx' or '5xx', a range such as '500-503', or 0 for a request that never completed. Left out, every code.
  • urls: text found anywhere in the address, such as '/api/session', or a pattern. Left out, every address. Accented and non-Latin paths can be written as they read.
  • count: whether a request the rule matches is a problem. True unless it says false.

The same rules can be set without a deploy, under Failed requests for each site on the Sites page. Only a workspace admin or owner can change them there; everybody else in the workspace can see them. They are read after the rules in your code, so a rule on the Sites page has the last word, and every page uses a change within five minutes. A rule the recorder could not use is refused when it is saved, with the reason, and one in your code is skipped with a warning in the browser console.

Three kinds of request are never a problem, whatever a rule says: one your own code cancelled, one cut short by the visitor leaving the page, and Spectra's own.

The error your code raises about a failed request, such as "Request failed with status code 401" or "Failed to fetch", is the same problem as the request and is not listed again. When a rule leaves the request out, the error goes with it. When an address answers with a web page where your code expected JSON, the error from reading it is listed as Expected JSON, got a web page, with the request named when only one page arrived at that moment. Chrome does not tell a page when its own response.json() fails, so in Chrome that error is seen only when your code reads the text and parses it.

Programmatic Code Triggers

Dispatch custom business events or update session properties dynamically from your application logic:

SDK Event Signals
const spectra = Spectra.record({ apiKey, siteId, endpoint }); // 1. Emit business event for rule evaluation spectra.track('PaymentFailed', { gateway: 'stripe', errorCode: 'card_declined' }); // 2. Set session metadata properties spectra.setProps({ userPlan: 'enterprise', cartValue: 499 }); // 3. Force-record session immediately with custom reason label spectra.flag('vip_checkout_stuck');

Zero-JS HTML Markup Triggers

Annotate HTML elements directly to capture friction points without custom script logic:

HTML Trigger Attributes
<button data-spectra-trigger="checkout_submit_clicked">Complete Purchase</button> <div data-spectra-flag="card_authorization_failed">Your card was declined</div>

Dashboard Trigger Rule Conditions

Configure trigger rules in the Spectra dashboard, under Sites and then Trigger rules. Every page uses a change within five minutes, with no redeploy:

Trigger Rule JSON Schema
[ { "id": "high-value-payment-failed", "on": { "event": "PaymentFailed" }, "where": [{ "key": "cartValue", "op": "gte", "value": 300 }], "reason": "high_value_payment_failed", "sampleRate": 1.0, "kind": "issue" } ]
Operator (op) Description
eq / neq Equals / Not equals target value.
gt / gte Greater than / Greater than or equal to numeric threshold.
lt / lte Less than / Less than or equal to numeric threshold.
contains String substring matching.
exists Property key presence check.

Framework Integration Reference

React and Next.js (App Router)

Install the package from npm:

Terminal
npm install @spectra-trace/recorder

Render this once, in your root layout. Importing the package on the server is safe; recording starts in the browser, when the effect runs. In development React runs an effect twice, and the recorder is stopped and started again, which leaves one recorder running.

components/SpectraProvider.tsx
'use client'; import { useEffect } from 'react'; import { record } from '@spectra-trace/recorder'; export function SpectraProvider() { useEffect(() => { const instance = record({ apiKey: process.env.NEXT_PUBLIC_SPECTRA_KEY!, siteId: process.env.NEXT_PUBLIC_SPECTRA_SITE_ID!, }); return () => instance.stop(); }, []); return null; }

Next.js (Pages Router)

pages/_app.tsx
import type { AppProps } from 'next/app'; import { useEffect } from 'react'; import { record } from '@spectra-trace/recorder'; export default function App({ Component, pageProps }: AppProps) { useEffect(() => { const instance = record({ apiKey: process.env.NEXT_PUBLIC_SPECTRA_KEY!, siteId: process.env.NEXT_PUBLIC_SPECTRA_SITE_ID!, }); return () => instance.stop(); }, []); return <Component {...pageProps} />; }

React with Vite, and other single-page React apps

Start the recorder before the app renders, so the first screen is in the recording.

src/main.tsx
import { createRoot } from 'react-dom/client'; import { record } from '@spectra-trace/recorder'; import App from './App'; record({ apiKey: import.meta.env.VITE_SPECTRA_KEY, siteId: import.meta.env.VITE_SPECTRA_SITE_ID, }); createRoot(document.getElementById('root')!).render(<App />);

React Router and Remix

In framework mode, start it from the root route, which every page renders.

app/root.tsx
import { useEffect } from 'react'; import { Outlet } from 'react-router'; import { record } from '@spectra-trace/recorder'; export default function App() { useEffect(() => { const instance = record({ apiKey: 'sp_live_xxxxxxxx', siteId: 'site_xxxxxxxx' }); return () => instance.stop(); }, []); return <Outlet />; }

Vue and Nuxt

A Vue app starts the recorder before it mounts:

src/main.ts
import { createApp } from 'vue'; import { record } from '@spectra-trace/recorder'; import App from './App.vue'; record({ apiKey: import.meta.env.VITE_SPECTRA_KEY, siteId: import.meta.env.VITE_SPECTRA_SITE_ID, }); createApp(App).mount('#app');

In Nuxt, use a client plugin. Nuxt 4 reads plugins from app/plugins/; Nuxt 3 reads them from plugins/ at the project root. A plugin in the wrong folder is never loaded and says nothing, so check which one your version uses.

app/plugins/spectra.client.ts (Nuxt 4)
import { record } from '@spectra-trace/recorder'; export default defineNuxtPlugin(() => { record({ apiKey: 'sp_live_xxxxxxxx', siteId: 'site_xxxxxxxx', }); });

SvelteKit

Start it when the root layout mounts, which happens once, in the browser.

src/routes/+layout.svelte
<script lang="ts"> import { onMount } from 'svelte'; import { record } from '@spectra-trace/recorder'; let { children } = $props(); onMount(() => { const instance = record({ apiKey: 'sp_live_xxxxxxxx', siteId: 'site_xxxxxxxx' }); return () => instance.stop(); }); </script> {@render children()}

Angular

Start it before the application boots. The recorder works outside Angular's change detection, so it does not make your components check themselves again.

src/main.ts
import { bootstrapApplication } from '@angular/platform-browser'; import { record } from '@spectra-trace/recorder'; import { appConfig } from './app/app.config'; import { App } from './app/app'; record({ apiKey: 'sp_live_xxxxxxxx', siteId: 'site_xxxxxxxx' }); bootstrapApplication(App, appConfig).catch((err) => console.error(err));

Astro

Add this to the layout every page uses. Astro bundles it and runs it once per page, with or without view transitions.

src/layouts/Layout.astro
<script> import { record } from '@spectra-trace/recorder'; record({ apiKey: 'sp_live_xxxxxxxx', siteId: 'site_xxxxxxxx' }); </script>

WordPress, Shopify, Webflow, Google Tag Manager and other site builders

Use the script tag from the Sites page rather than the package. Paste it wherever your platform lets you add code to the head of every page: the theme header in WordPress, theme.liquid in Shopify, the site-wide custom code setting in Webflow, Wix, Squarespace or Framer. In Google Tag Manager, add it as a Custom HTML tag firing on all pages; the tag keeps its settings when Tag Manager adds it after the page has loaded.

SDK Configuration Parameters

The record() initialization method accepts an object with the following parameters:

Parameter Type Default Description
apiKey string Required The key issued for this site. Shown once when issued; rotate it from the Sites page.
siteId string Required Unique site identifier linking recorded sessions to your organization workspace.
endpoint string 'https://api.spectra-trace.com/v1/ingest' Where recordings are sent. Shown with your snippet on the Sites page. Leave it out to send to Spectra directly, or set tunnel instead for an install on your own domain.
tunnel string null A path on your own site that forwards to Spectra, so recording is not affected by content blockers. See Serving Spectra from your own domain.
tracing boolean | object off Send W3C traceparent and baggage headers so a replay and your server logs name the same request. true covers your own address; { origins: [...] } adds others, which must allow the headers. See Linking a replay to your server logs.
journeyOrigins string[] [] The other origins your visitors' journeys cross. Named on both sides, a visit that moves between them stays one recording. Nothing is ever carried to an origin not in this list.
userId string null Internal user identification string for cross-session correlation.
privacyMode string 'mask-pii' One of 'strict', 'mask-pii', 'mask-inputs' or 'allow'. The default masks every input value and everything typed into an editable region, and scrubs recognisable personal data out of page text and out of attributes such as title and alt. 'strict' redacts all plain text across the document tree, attributes that carry words included.
maskInputs boolean true Masks the values of form fields, and the text a visitor types into an editable region (an element with contenteditable, which is how chat boxes and rich-text editors are built), before recording. Add data-spectra-unmask to an editable region to record what is typed into it. Password fields, every card field and one-time codes are masked even when this is false, and so is the name of a file a visitor chooses.
captureBeforeConsent boolean false Reproduce bundle only, with hasConsent: false. Records from the page's load while the consent banner is unanswered, holding everything in page memory: nothing written to the device, nothing sent, until consent(true), which sends it all from the start. consent(false), or leaving the page first, drops it unsent. Lawfulness per region is your decision.
inputsAsTyped boolean true (Reproduce) Reproduce bundle only. What visitors type is recorded as typed, emails and phone numbers included; false keeps typed text scrubbed of personal details. Passwords, card fields and one-time codes are never recorded whatever this says. The base recorder ignores it.
respectDoNotTrack boolean false Stop recording a visitor whose browser sends Do Not Track or Global Privacy Control. Off by default because Brave sends Global Privacy Control for every visitor and Firefox sends Do Not Track in private windows, so turning it on for you would quietly remove a large share of your traffic from your own recordings. Turn it on when that is what you want.
hasConsent boolean or function None A consent gate. Recording waits until this is true, so a consent banner can hold it back. Pass a function to have it checked when recording starts. Nothing is written to the visitor's browser while it is false, including the anonymous visitor id.
pulse boolean true Send a small anonymous count of page views and interactions per page, used to detect a page whose conversion has dropped. No content, no identity.
analytics boolean true Measure behaviour on this site: where people click, how far they scroll, what frustrates them and how fast each page is. Only ever collected for a workspace that has the UX analytics add-on; set false to switch it off for one site that does.
frameBridge boolean | { parentOrigins: string[] } true On a page, let iframes from your other origins on the same key record into its session. Inside such a frame, name the pages allowed to adopt it, for example { parentOrigins: ['https://app.example.com'] }; a frame that names none sends nothing to whatever framed it. Inputs inside those frames are always masked. A frame on the page's own origin needs none of this: the page records what it shows and what is done in it, and a copy of the snippet inside it does not start a recording of its own. One that started before the page's own snippet stops as soon as the page starts recording.
requireAuth boolean false Record only when a userId is set; anonymous visitors are not recorded.
securityKey string undefined A second value that must be given, and must equal your apiKey, before recording starts. Anything else stops recording silently, so set it only if you have a reason to require the key twice.
customFilter function undefined A function receiving the configuration and returning whether to record this visit.
ignoreBots boolean false Record automated browsers too. By default crawlers and headless browsers are skipped.
idleThresholdMs number 3000 How long with no activity before the visitor is considered idle. Idle time is skipped in playback.
flushIntervalMs number 5000 How often queued activity is uploaded while a session is being kept.
smartRecording object enabled Keep only sessions where something happened. Pass { enabled: false } to keep every session from page load, within your plan's limits.
triggerRules array [] Rules evaluated against track() and setProps(). Rules saved in the dashboard are merged in automatically.
onError function undefined Called with any error the recorder encounters. Recording never throws into your page.
release string None The build this page came from, for example 2026.09.25. Sessions and issues group by it, which is how you tell whether a problem arrived with a deploy, and the Integrity page says how each broken control stands in your newest release against the others.
environment string None Which deployment this is: production, staging, a review app. Filterable in the session list, so a staging visit is never read as a customer.
sampleRate number 1 What fraction of visitors to record, 0 to 1. Decided from the visitor rather than a coin toss, so you get that fraction of visitors with whole journeys rather than that fraction of page views with holes, and a visit already under way is always finished. Most sites should leave this alone: an uneventful visit already costs almost nothing.
networkCapture object metadata only What a replay may keep about your requests beyond the address, status and timing it already records. { bodies: { urls: [...] }, headers: [...], denyKeys: [...], allowKeys: [...], bodyLimit: 8192, sockets: true }. Nothing is kept for an address you do not name. bodyLimit is how many bytes of each body are kept (8192 unless raised, up to 1048576); raise it where a visit must be replayable from its recording. See After the snippet: what to set up.
consoleLevels string[] ['warn', 'error'] Which console levels are recorded. Add log, info or debug when your own diagnostics go through them. Uncaught errors and unhandled rejections are always recorded whatever you choose.
keyboard boolean true Records the keys that are decisions: Enter, Escape, Tab, the arrows, and combinations such as Ctrl+Z. What a visitor types is never recorded this way on any setting; characters reach a recording only as a field value, under the masking rules.
longTasks boolean true Marks the moments the page stopped responding. Without them a frozen replay is indistinguishable from a visitor who stopped moving. Measured by Chrome and Edge; Safari and Firefox do not report it.
blockAllMedia boolean false Replaces every image, video and canvas with a placeholder of the same size, for a site where the pictures themselves are confidential. The layout is unchanged. Images referenced from CSS backgrounds are not replaced, but none of them is kept with a recording: a replay shows them only for as long as your site still serves them. Icons drawn in SVG are part of the page and are kept, including those drawn from a sprite file.
persistence 'local' | 'session' | 'memory' 'local' How long anything the recorder writes may stay on a visitor's device. session keeps nothing past the visit, which lets you keep recording when a consent banner has been refused. memory writes nothing at all, and makes every page its own session.
dashboardUrl string Spectra Where getSessionUrl() points. Only needed for a self-hosted deployment.
slowRequestMs number 10000 How long a request somebody is waiting on may take before the session is kept. Only counts requests that started while a visitor was waiting, so background polling does not flag every visit.
failedRequests array every failure counts Which failed requests are a problem: rules by code and address, the last match deciding. See Choosing which failed requests count, under Recording rules.
stuckLoadingMs number 15000 How long a loading indicator may stay on screen before the session is kept as a stuck loading screen.
debug boolean false Prints what the recorder is doing to the console. For checking an install; not for production.
onDiagnostic function undefined Called with what the recorder decided and why, as it happens. The programmatic form of debug.
blockClass string | RegExp 'spectra-block' A class name that replaces an element, and everything inside it, with an empty box of the same size. A string matches a class exactly, so secret matches class="secret" and not class="secret-note"; a regular expression is tested against each class on the element. The default already works, so you can hide an element today by giving it class="spectra-block". The box keeps the size the page was laid out around, and for a table cell or a dropdown option it keeps the few attributes that hold the layout together, including that option's value.
blockSelector string | null null The same as blockClass, written as a CSS selector, for hiding something you cannot add a class to. Checked once when recording starts: a selector this browser cannot parse is ignored and said once in the console, and everything else you asked to hide still is. That is the one place a mistake here hides nothing rather than too much, so check the console when you set it.
maskTextClass string | RegExp 'spectra-mask' A class name that replaces the text inside an element with mask characters, while leaving the element itself in the recording. Put it on a container and every piece of text beneath it is masked, along with the attributes that carry words, such as title and alt, and the values of fields inside it even when maskInputs is false. A string matches a class exactly, and a class holding a slash or a colon is safe. The default already works, so class="spectra-mask" masks text today.
maskTextSelector string | null null The same as maskTextClass, written as a CSS selector. It matches the element or any element above it, so a selector naming a container masks everything inside, fields included. Validated when recording starts, with the same console warning as blockSelector.
maskChar string '*' The character used when something is masked and you have not supplied a function to do it. One per character of the original, so the length of a hidden value is still visible in the recording. An email address and an authorisation token are replaced with a fixed run instead, so their length is not. Keep it to a single character: a longer string is repeated per character and doubles the length of every masked value.
maskInputFn function one mask character per visible character Called with a field's value and its element, once per value, and what it returns is what the recording keeps. It is called for the fields that are masked outright. It is deliberately not called for an email or telephone field, or one your markup marks as a card number: those are replaced with a fixed run so their length says nothing either. Returning the value unchanged turns masking off for every field it covers.
maskTextFn function one mask character per visible character Called with the text of each masked text node and its parent element, and with the value of each masked attribute that carries words, such as a title, and its element. Per text node, not per element, so a sentence a framework has split into three pieces arrives in three calls and your function cannot see the whole of it. When you set this, maskChar no longer applies to text.
scrubPII boolean true Finds and masks personal detail in text the recording would otherwise keep as written: card numbers that pass a checksum, so order numbers and product codes survive; social security numbers in the United States format; email addresses; telephone numbers written the way people write them, with a country code (+91 98765 43210, +44 20 7946 0958) or as a national number of ten to twelve digits, but not digits that are part of an identifier, a date, an address or an amount; and authorisation tokens. It covers text, the values of fields, and the attributes that carry words: title, alt, placeholder, aria-label and every data- attribute. In a link it covers the query, and an email or telephone link loses its address or number, while the page the link points at is kept. It does not know names, postal addresses or account numbers. Use customPIIRegexes, a class or a selector for those.
customPIIRegexes RegExp[] [] Your own patterns, masked wherever they appear in text, after the built-in ones. Every match is masked, whether or not a pattern has the g flag. These run on every piece of text the recorder keeps, console messages and error messages included, so a pattern that backtracks badly costs the visitor's browser. Turning scrubPII off turns these off for page text and attributes; console and error messages are always scrubbed.
recordCanvas boolean true Records what a two-dimensional canvas is showing, so charts, maps and signature pads appear in the replay rather than as an empty rectangle. Turn it off for a canvas whose contents are confidential, or hide it with blockClass.
recordWebGL boolean false Records three-dimensional canvases as well. Off by default because reading one back costs the visitor's browser more than a flat canvas, and because a page can be built so that reading it back is not possible at all. A three-dimensional scene is an empty rectangle in the replay until you turn this on.
inlineStylesheets boolean true Keeps the text of a stylesheet the page loaded, so the replay looks like the page did even after you deploy a new one. Turn it off and the replay asks your own site for the stylesheet when somebody watches, which looks wrong once that file has changed or moved.
keepIframeSrc function keeps no frame addresses Called with the address of each frame on the page. Return true to keep that address in the recording, so the replay loads the frame live. By default no frame address is kept, because loading a third party's frame at watch time shows whatever it shows today rather than what the visitor saw.
userDedupeWindowMs number 0 A cooling-off window, in milliseconds, for one signed-in person. Zero turns it off. Set it and the same userId is recorded at most maxSessionsPerWindow times within the window, which is for a page somebody reloads repeatedly.
maxSessionsPerWindow number 3 How many recordings one signed-in person may produce inside userDedupeWindowMs. Ignored while that window is zero.
smartRecording.frictionDetection boolean true Whether a recording is kept when a visitor clicks something repeatedly, clicks something that does nothing, or is shown a form error. Turn it off to stop those from being reasons on their own.
smartRecording.struggleDetection boolean true Whether a recording is kept when a visitor tries the same thing several ways, or puts real effort in and then leaves without finishing.
smartRecording.loadingDetection boolean true Whether a recording is kept when a loading indicator stays on screen longer than stuckLoadingMs.

User Identity and Event Tracking

Link incoming replay recordings to your internal user accounts using identify():

User Identification
const spectra = Spectra.record({ apiKey, siteId, endpoint }); // Call after user login completes spectra.identify('user_94821');

What the recorder returns

Spectra.record() returns an instance, also reachable as Spectra.current(), with these methods:

Method What it does
identify(userId)Attach the whole session, including everything before this call, to one of your user ids. Pass a string; the first 128 characters are kept.
track(name, props)Record a business event. It appears in the replay's event list, is evaluated against your rules, and is counted on the Overview's Events panel. Give it a value and a three-letter currency to total what it was worth.
setProps(props)Set properties of the session such as plan or cart value. Rules watching a property re-evaluate when it changes.
flag(reason, details)Keep this session now, under a reason you name. triggerRecording is the same call.
emitCustomEvent(tag, payload)Add a custom marker to the timeline.
pause() / resume()Stop watching the page without ending the session, for a screen a recording must not contain. Everything recorded before the pause still arrives, the visit stays one visit, and nothing about the page is observed while paused. Resuming photographs the page again.
getSessionUrl()Where to watch this session, for your own logs and tickets. Null when nothing is being recorded.
captureError(error, context)Report an error your own code caught. Uncaught errors and unhandled promise rejections are recorded automatically; an error your framework catches never reaches the browser, so this is how it reaches the replay. See Reporting errors your code catches.
setContext(labels)Say what state the page is in now, such as a plan or a feature you are trying out. null removes a label. See After the snippet: what to set up.
setExperiment(name, variant)Say which arm of an experiment this visitor is in, under the testing tool's own names.
handoff(url)The address to send the visitor to so this visit carries on at another address you listed in journeyOrigins. Any other address comes back unchanged.
journeyToken()This visit as a token your server can put on a redirect between two of your domains. See Recording a whole journey.
consent(granted)The visitor's answer to your consent banner, given after the page loaded. false stops recording, sends nothing more, and removes what the recorder stored on the device.
takeFullSnapshot()Capture the page again from scratch, for pages that rebuild their whole DOM.
stop()Stop recording for this page.
sessionIdThe id of the current session, for your own logs.

Reporting errors your code catches

Uncaught errors and unhandled promise rejections are recorded for you, and the session they happened in is kept. But a framework catches: that is what an error boundary is for, and an error it catches never reaches the browser, so nothing outside your code can see it. The better your application handles its failures, the less a replay knows about them. One line puts them back.

React
class Boundary extends React.Component {
  componentDidCatch(error, info) {
    Spectra.current()?.captureError(error, { component: info.componentStack });
  }
  render() { return this.props.children; }
}
Vue
app.config.errorHandler = (error, instance, info) => {
  Spectra.current()?.captureError(error, { where: info });
};
Angular
@Injectable()
export class SpectraErrorHandler implements ErrorHandler {
  handleError(error: unknown) {
    Spectra.current()?.captureError(error);
  }
}

// app.config.ts: register it, or Angular keeps its own handler
providers: [{ provide: ErrorHandler, useClass: SpectraErrorHandler }]
A script tag, with no bundler
window.spectraQ = window.spectraQ || [];
try { risky(); } catch (error) {
  window.spectraQ.push(['captureError', error, { where: 'checkout' }]);
}

The error is recorded with its message, its stack and any properties you attached to it, so a tagged error keeps the status code that explains it. The session is kept for review, and the message is scrubbed for personal data exactly like every other message.

Errors from scripts on another domain

An error thrown by a script loaded from another domain, such as a CDN or a third-party widget, reaches every error tool as Script error. with no message, file or line. The browser hides the details on purpose, so that one site cannot read another's code through its errors. To see them in full, add crossorigin="anonymous" to the script tag that loads it, and have its host send an Access-Control-Allow-Origin header. Many CDNs send it already; your own servers may need it added.

Add the attribute only where the header is sent. A browser refuses to run a script marked crossorigin whose host does not send it.

Replay and filters

Replay lists every recording kept for the selected site, newest first, with why it was kept. Filter by the reason, by one of your user ids, by a release or environment, or by a page URL, which matches anywhere the visit went rather than only where it started. The second row of filters narrows by what they were on and how the visit went: phone, tablet or desktop, browser, operating system, how long they stayed, and whether anything broke. Clicking a visitor shows everything else that person did. A site with no recordings yet shows the install steps in place until the first one arrives.

Issues

Issues groups recordings by what went wrong, so the same error on the same page is one row however many people hit it. Open a row to see the pages affected and the recordings behind it. Choose the window in the bar at the top, from the last day to the last ninety.

An issue you cannot or need not fix, such as an error from somebody else's script, can be ignored: open it and choose Ignore. It leaves the list, the daily email and the weekly brief for everybody in the workspace. Its recordings and counts are kept, Status set to Ignored shows it again, and Stop ignoring brings it back. Only a workspace admin or owner can ignore an issue. To stop a kind of failed request counting at all, rather than one issue, see Choosing which failed requests count.

Traffic

On the paid plans the Overview leads with your traffic, counted for every visit: visitors, visits, pageviews, bounce rate, the time on site of the middle visit, and how often your events fired, each against the window before. Each figure is drawn as a line over the window: by the hour for the last 24 hours, by the day for longer, in UTC. On a very large site a long window is estimated from a share of its visitors, and the Overview says so beneath the chart.

Your traffic is kept for three months, or for your plan's history window if that is longer.

Below the figures: the top, entry and exit pages; where visits came from, with search engines and networks named, a visit with no referrer credited to the utm_source of the link it arrived on, and visits from AI assistants such as ChatGPT, Perplexity and Gemini on their own tab; the five campaign tags, utm_source, utm_medium, utm_campaign, utm_content and utm_term; countries; devices, systems and browsers; and your events, with the share of visitors who fired each and what they were worth.

Visits from crawlers, link previews, audit tools such as Lighthouse, WebPageTest and GTmetrix, and headless browsers are not counted, and do not use your allowance. The country comes from the visitor's time zone, not their address, which Spectra never reads. It is country only, and a visitor on a VPN is placed where their computer's clock is set. To count an event, call spectra.track('Purchase', { value: 49.99, currency: 'USD' }); the value and the three-letter currency are optional, and are the only properties counted.

Analytics

Replay and Issues answer questions about one visit at a time. Analytics answers the ones that only exist across many: which control nobody finds, how far down a page people actually get, which page quietly loses them. It measures every visit rather than the share worth recording in full, so a page with no recordings still has numbers.

Overview is how visits went for the window you choose: how many ran into something that went wrong, how long people stayed on a page and how much of that was active, and the three speed measures a visitor actually experiences. Visitors, sources and countries are on the main Overview.

Pages ranks every route by traffic, with time on page, how far people scrolled, and how many arrived from elsewhere on your site and were gone within five seconds. Open a row for that page on its own.

The heatmap is drawn over your real page rather than a screenshot. A masked capture of the page is rebuilt in the browser and the counts are attached to the elements themselves, so the map stays correct when your layout changes and can be shown at desktop, tablet or phone width. Switch between clicks, rage clicks, dead clicks and clicks that caused an error. Each busy element wears its share of the page's clicks in the view you are on, the rail beside the picture ranks the busiest and explains the colours, and hovering a row lights its element on the picture. Click any element to list the visits that clicked it, with replays first.

Scroll draws how far down the page visits got, as bands over the page itself: the share of visits that reached each part, labelled every quarter, and a dashed line where a typical first screen ended. Anything that matters wants to sit above that line.

The picture of a page appears after a few visits: a small share of visitors contribute one, so a busy page costs almost nothing to keep current. Refresh picture takes a new one, and Pin picture keeps the current one whatever changes.

Findings is the ranked list of what is going wrong, ordered by how many people each thing affects rather than how often it happens: forty visitors clicking a broken button once each matters more than one visitor clicking it forty times. Every finding states the rule it applied and links to a replay of somebody hitting it.

The replay player

A recording plays back what the visitor saw. Idle and away time is skipped automatically, so a twenty-minute visit with two minutes of activity plays in two. The panel beside the player has six tabs.

Activity is what the visitor did, in order, lighting up as playback reaches it: clicks, typing, page changes, the keys that are decisions such as Enter and Escape, and the moments the page stopped responding. Pages is each page of the visit, and Issues is what went wrong in it, each one a place to jump to. Details is who they were, what they were on, and how fast the page was for them, banded against the same thresholds Google uses so a number here means what it means in Search Console. Console is what the page logged, with errors shown as errors rather than as empty braces. Network is everything the page asked for and everything it loaded, each row saying what asked for it and how long the visitor waited, and opening to show the timing, the headers, and the bodies for the addresses you have named.

Copy bug report puts the page, the user, the errors and a link to this recording on the clipboard. Share link makes a read-only link that opens this one recording without a sign-in.

Sites and keys

Each site has its own key and its own snippet. The key is shown once, when it is issued; only a hash is kept, so there is no copy to show later. Replace key issues a new one and stops the old one immediately, so update the snippet in the same change. Rules for what to keep live under each site; every page uses a change within five minutes, without a deploy.

Failed requests under each site says which failed requests count as a problem, by code and address (see Choosing which failed requests count, under Recording rules). Anybody in the workspace can see the rules; only an admin or the owner can change them.

Being told when something breaks

Spectra already notices when a page stops working. It watches how often the people on a page click the things people normally click there, and how often a given failure happens compared with how often it usually happens, and when either changes sharply it starts recording the visitors it affects. What it could not do was tell you, so all of that waited until somebody opened the dashboard.

Under each site, in Tell me when it breaks, paste a Slack incoming webhook or any address of your own that accepts a POST. You get one message when a problem starts, naming the page and how much worse it has got, with a link to the recordings. One message per problem, not one per check: something that repeats itself every few minutes is something people turn off, and an alert nobody reads is worse than no alert because it feels like coverage.

When several things break at once, you are not sent a message for each. After a message, a site is quiet for ten minutes, and anything found in that time arrives together as one message that lists it. A site is sent at most ten messages a day. The tenth says it is the last, and anything else that breaks that day is on the Issues page. For a receiver of your own: every message carries an alerts list with each problem in it, and a message about one problem also carries it as alert. The text is written for Slack, so &amp;, &lt; and &gt; stand for &, < and >; the fields beside it carry every value as it was.

If your pages say which release they came from, you are also told when your newest release has a control people could not use that your other releases did not, or has one more often: one message for the release, naming each control, its page, and how many visits met it in that release against the others, counted on the screen sizes it happens at. Each one is told once. Nothing is said until enough visits have come in to be sure and the answer has held for half an hour, so a release a few minutes old takes a little while.

To see a message arrive before anything has broken, choose Send a test message under the same heading. It goes at once to every place your alerts go, and the screen says how it went at each. It never holds back a real alert, and one can be sent every few seconds.

The same alerts can go to Telegram as well, or instead. Under Tell me when it breaks, choose Connect Telegram, then open Telegram from the link it gives you and press Start for a private chat, or choose a group to add Spectra's bot to your team's. The link works once, for thirty minutes. If Start does nothing, because the Telegram app is not installed, choose Open in Telegram Web, or open Spectra's bot yourself and send it /start followed by the code, which is the end of the link after ?start=. The Sites screen shows the whole command. Each alert reaches Telegram once, exactly as it reaches your webhook, and Disconnect stops it without touching the webhook. If the bot is removed from the group or blocked, the next alert finds that out: Spectra stops sending there, and the site shows Telegram as not connected.

If you set a signing secret, every message carries a X-Spectra-Signature header: sha256= followed by an HMAC-SHA256 of the exact body, keyed with your secret. Recompute it over the raw bytes you received and compare. That is how your receiver knows the message came from us. The secret is never shown again after you save it, not even to you.

Team and invitations

Invite colleagues by email from the Team page. An invitation link is good for seven days and is also shown to you, so you can pass it on if the email does not arrive. Roles: an owner pays and can see billing; an admin manages people, sites and keys; a member watches recordings and edits rules; a viewer reads. Anyone with admin or above can change a role or remove a person, but not themselves. You only see what your role allows, so a page you have no use for is not shown to you.

You can be in two workspaces: one of your own and one you were invited to. When you are in both, a workspace picker appears at the bottom of the sidebar, and everything on the page follows the one you choose. To join a different workspace, leave one first from its Team page. The owner of a workspace cannot leave it, since the subscription belongs to them.

Plan and quota

Billing shows your plan, the sessions this period against your allowance, and your retention window. When the allowance is used up, new recordings pause until the period resets or you move up a plan; nothing already kept is affected. On the paid plans your traffic keeps counting past it, up to twenty times your plan's visits, so the Overview's figures stay whole for the month; past that, nothing more is counted until the period resets, and the recorder stops sending anything at all. A plan change applies immediately: the new plan is charged in full and starts a fresh month, with no credit for the days left on the old one, and the confirmation says so before you agree. Invoices and payment details are managed from the same page.

The free plan is session replay: recordings, the replay player, sessions and issues. The integrity check, weekly summaries and Spectra AI are part of the paid plans, and heat maps and page analytics are the analytics add-on.

Reproduce: test every pull request with real visits

Reproduce records what real visitors do on your live site and replays it against every pull request. If something people did yesterday no longer works in the pull request, the check fails and tells you which journey and which step. Nobody writes or maintains tests. It is an add-on, switched on per site: ask us to switch it on, then follow these steps. They take about ten minutes.

What you need

  • A site in Spectra (Sites), and its API key. The key is shown once, when the site is made; if you no longer have it, issue a new one on the Sites page.
  • A GitHub repository for the site, where you can add secrets and workflows.
  • A build command that writes your site to a folder (dist, build or out), or a preview URL for each pull request.

Step 1. Put the Reproduce tag on your live site

Open the dashboard, choose the site in the bar at the top, and go to Reproduce, then Set up. Copy the tag from step 1 there and put it first inside <head> on every page, in place of your current Spectra tag. Replace YOUR_API_KEY with the site's key. Leave out async: a visit is replayed from its first moment, so the script has to start first.

The Reproduce tag
<script src="https://api.spectra-trace.com/v1/sr.js" data-api-key="YOUR_API_KEY" data-site-id="YOUR_SITE_ID" data-endpoint="https://api.spectra-trace.com/v1/ingest"></script>

Deploy the site. Every visit from then on is recorded as a possible test. What visitors type is kept as typed, so a replay can sign in the way they did; passwords, card fields and one-time codes are never recorded, only their shape. To keep typed text scrubbed, add data-inputs-as-typed="false" to the tag.

Step 2. Create a CI token

Still on Reproduce, Set up, step 2: type a name such as github-actions and press Create token. Copy the token straight away: it is shown once. If you lose it, revoke it on the same page and create another.

Step 3. Give GitHub the token and the site

In your GitHub repository, open Settings, then Secrets and variables, then Actions, and add a secret named SPECTRA_REPRODUCE_TOKEN holding the token from step 2.

Step 4. Add the workflow

Create the file .github/workflows/spectra-reproduce.yml in the repository with the workflow from Set up, step 3 (it has your site's id filled in), or this one with YOUR_SITE_ID replaced. Change two lines: base is your live site's address, and candidate is the folder your build writes. If your build command is not npm run build, change that line too.

.github/workflows/spectra-reproduce.yml
name: Spectra Reproduce on: pull_request # The report is posted on the pull request as one comment, updated on every push. permissions: contents: read pull-requests: write jobs: reproduce: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 # Build this pull request the way you build production. - run: npm ci && npm run build - uses: rahulhind/reproduce-action@v1 with: site: YOUR_SITE_ID token: ${{ secrets.SPECTRA_REPRODUCE_TOKEN }} # Your live site, where the Reproduce tag records visits. base: https://www.your-site.com # The folder your build writes (dist, build, out), or a preview URL. candidate: dist

The folder is uploaded to Spectra and served privately for the length of the check, so you do not need a preview deployment. Uploaded builds are kept for seven days.

The report is posted on the pull request as one comment and rewritten on every push, never a new one each time; without pull-requests: write it stays in the job summary. To see the reports before letting Spectra fail a job, add fail-on: never under with:. If you deploy previews on Vercel or Netlify, give the preview's address as candidate and run the workflow on: deployment_status, with if: github.event.deployment_status.state == 'success' on the job and candidate: ${{ github.event.deployment_status.environment_url }}. A deployment is not a pull request, so that report goes to the job summary and the check, not to a comment.

Step 5. Let people use the site

Checks are made of real visits, so the first one needs some. Use the site yourself for a few minutes, or wait for visitors. A visit counts once it has ended, about 20 minutes after its last activity. Reproduce, Journeys shows the tasks found so far; Add recent visits adds the latest ones now. Each check also adds them on its own.

Step 6. Open a pull request

The workflow runs on every pull request, and its result appears on the pull request and under Reproduce, Checks. Each journey gets one verdict:

  • Same: it does what it did.
  • Broken: a step that worked now fails. It can't be done, an answer that succeeded fails, an error appears, or a press does nothing. The check fails.
  • Changed: it works, differently: a new page, a different request, or other words on screen at a step (an error message that now says something else, a message that appears and goes, a renamed heading). Shown for review; the check passes.
  • Couldn't replay: Spectra could not judge it, for example because the pull request asks the server for something no recorded visit asked. It goes to Reproduce, Inbox, where you answer it once; it is never counted as a pass.

Spectra compares what each step showed on screen, not only where it led: a failed payment that said "Something went wrong" in production and now says "Account balance should be greater than 500" is reported with both messages. Text nobody can see (kept for screen readers, folded away) is left out, and so is text that changes by itself between two replays of the same build, such as a clock or a rotating banner. Visitors' own moves in the browser, Back, Forward and an address typed, are replayed as they made them.

A journey that cannot be replayed even on your live site is tried again on another real visit of the same journey before it is reported. If none replays, you can Leave this journey out from the inbox, with a reason: it is left out of checks for 30 days, every report in that time lists it, and then it is judged again. When one change shows on many journeys (a button renamed everywhere), one Intended on every journey, for the difference named beside it, covers all of them. In the pull request's report Spectra quotes your app's own words, never what a visitor typed or what your server answered them: those read "[what the visitor typed]" and "[the server's answer]", and the dashboard shows them in full.

Every journey that is not the same is said in plain words: which step, what it did in production, what it does in the pull request, and why, as the browser saw it (for example, "another element covers it and takes the click"). Open the check under Reproduce, Checks to watch both replays side by side (Play both), and Watch the real visit opens the recorded visit at that moment. Copy fix prompt for an agent copies the same account, written for an AI coding agent to act on; the pull request's report carries it too. Replays are kept as video for 30 days.

If something is not right

  • The Reproduce page shows only Set up, with "Reproduce isn't on for this site yet": the add-on is not on for the site chosen in the bar at the top. Check the site; the page names the site id it is looking at.
  • The check says there are no journeys: no visit has been recorded and ended yet. Check that the tag is on the live site (step 1), use the site, and wait about 20 minutes.
  • The check fails with "This token does not open this site": the secret holds a revoked token, or a token from another site. Create a new one (step 2).
  • A journey is "Couldn't replay": open the Inbox. It says what was missing and lets you give the answer once, or decide that the difference is intended.

What counts as friction

A recording is kept when something went wrong or somebody struggled. These are the reasons, in the words the session list and the Issues page use. Nothing here needs configuring; they are what the recorder watches for on every page.

ReasonWhat it means
Rage clickThe same control clicked over and over in a burst. Almost always means the control answered too slowly to feel like it had answered at all, rather than that it was broken.
Dead clickA click a visitor made on something that looks interactive and produced no response: nothing navigated or downloaded, nothing opened in another tab or window, nothing was copied or printed, nothing played, redrew or restyled, nothing changed, no request started. Judged after a pause, and a click that started a request or sent the visitor to another page is never called dead just because the answer was slow.
Form errorA field was rejected. Reported once per attempt rather than once per keystroke, so clearing a box to retype it is not a finding.
Form thrashingThe same field edited and re-edited without the form moving on. The shape of somebody who cannot work out what a field wants.
Abandoned after effortReal work put in, and then the visitor left with nothing completed.
StruggleRepeated effort with no progress: submitting and returning to the same screen, hunting between the same controls.
Stuck loadingA loading indicator that never resolved. See the stuckLoadingMs option for the threshold.
Slow responseA request somebody was waiting on took far too long and then succeeded. A failure is already a finding; this is the one that is invisible otherwise, because nothing broke.
Abandoned a blank pageThe page never rendered anything and the visitor left.
Page did not renderThe page loaded but the things people interact with are not there, which is what a failed deeplink or a broken route looks like.
JS error, Unhandled rejection, Console errorAn error was thrown, a promise rejected with nobody handling it, or your code logged an error.
Failed request, Unreachable requestA request came back as a failure, or never completed at all: refused, unresolvable, or the network went away. The two are separated because only the second means the address could not be reached. A page that itself arrived as an error says how visitors reached it: a link on one of your pages, a campaign, another site, or an address typed in. A broken link on your own pages, or one from a campaign or an AI assistant, is listed first.
Resource failedA script or stylesheet the page needs did not load.
Blocked by policyYour own Content-Security-Policy refused something the page tried to load.
Install testA recording you asked for from the browser console, to prove the snippet works. See Verifying Installation.
Recorded for IntegrityA control was covered by something else, hidden, or had no size at that moment, so the visit is kept for the integrity check on the paid plans. It is not a problem on its own: the Integrity page lists a control only once visitors were kept from using it.

You can add reasons of your own with track(), setProps() or a data-spectra-trigger attribute, and turn them into recordings with rules. See Recording rules.

What analytics collect

Analytics answers questions that only exist across many visits: which pages people reach, where they stop, where they click, how far they scroll, how fast pages were. It works differently from recordings and it is worth knowing how, because the honest answer to "are you tracking my visitors" depends on it.

A recording is kept only when something happened. Analytics needs every visit, so each page life sends one small anonymous summary as the page is left: which page, how long, whether the visitor interacted, how far they scrolled, which elements were clicked, the speed the browser measured, and where they came from. It is a summary, not a stream: there is no replay in it and no way to reconstruct one from it.

What it never contains: no name, no address, and no text from the page. It carries the same anonymous visitor id your recordings do, a random value with nothing about the person in it, plus your own user id if you set one with identify(). That visitor id is kept on the device so a returning visitor is recognised; with persistence: 'session' or 'memory' it ends with the visit. Element labels go through the same masking your recordings do, so a button whose text you have masked is a button whose label is masked here too.

It is collected only for sites whose workspace has the Analytics add-on. A site without it sends nothing at all, rather than sending it and having it discarded at our end. Set analytics: false to opt a site out even with the add-on active.

Heatmaps need one more thing: a picture of the page to draw on. A small fraction of visitors to a page that has no usable picture contribute one, which is the page as it was built in their browser, with the same masking applied. Everybody else contributes nothing to it.

On-Device Privacy and PII Masking

Spectra hides sensitive information in the visitor's browser, before anything is sent. Every input value is masked by default, and so is what a visitor types into an editable region such as a chat box or a rich-text editor. Passwords, card fields and one-time codes always are, whatever else you set. Card numbers, email addresses and authorisation tokens found in the page's text, and in attributes such as titles and image descriptions, are masked too. These attributes change what is kept for one element and everything inside it:

HTML Attribute Behavior
data-spectra-mask Replaces text contents, the attributes that carry words, and form field values with the mask character, an asterisk unless you set maskChar. On an iframe, or on anything that contains one, it covers the whole page the frame shows.
data-spectra-block Completely hides the element and all child nodes, replacing it with a placeholder box. On an iframe, or on anything that contains one, nothing inside the frame is recorded.
data-spectra-unmask Shows an element and everything inside it unmasked: its text, even in strict mode, its field values, and what is typed into it if it is an editable region. Recognisable personal data is still scrubbed from its text, and passwords, card fields and one-time codes stay masked whatever it says.

Erasing one person's recordings

When somebody asks you to delete their data, this removes every session recorded for them on a site, wherever it is kept, and the analytics of every visit of theirs, recorded or not. It needs the admin role in the workspace that owns the site.

Authenticate with your dashboard session token, not the ingest key. The ingest key can only write recordings; it cannot read or delete them, which is what keeps a key pasted into a public page from being able to erase your history. Open your browser's developer tools while signed in to the dashboard and copy the Authorization header from any request. It stops working when your sign-in does, so copy a fresh one when you need it.

Delete one visitor's recordings
curl -X DELETE "https://api.spectra-trace.com/v1/user-data?site=site_xxxxxxxx&user=user_94821" -H "Authorization: Bearer YOUR_DASHBOARD_TOKEN"

The reply says how many sessions were found and how many were removed. If a stored copy cannot be deleted the whole request fails rather than reporting a partial erasure as complete, so it is safe to retry.

Deleting a site

Deleting a site deletes everything recorded under it, everywhere it is kept. It cannot be undone and there is no recycle bin. Also admin or higher.

Delete a site and its recordings
curl -X DELETE "https://api.spectra-trace.com/v1/sites?site=site_xxxxxxxx" -H "Authorization: Bearer YOUR_DASHBOARD_TOKEN"