@flamioai/web-sdk
v0.9.0
Published
Lightweight SDK for continuous UX session recording on production sites
Maintainers
Readme
@flamioai/web-sdk
Lightweight JavaScript SDK for continuous UX session recording on production sites. It captures rrweb DOM recordings, console errors, network failures, Web Vitals, scroll depth and SPA navigation, then batches and uploads them to FlamioAI for real-time UX analysis.
Dashboard: create a project, grab your projectKey, and watch the recorded
sessions & AI analysis at admin.flamio.org
(Projects → On-Live → Setup).
Installation
npm install @flamioai/web-sdkOr load it directly in the browser:
<script type="module">
import { Flamio } from 'https://esm.sh/@flamioai/web-sdk';
Flamio.init({ projectKey: 'flm_...', endpoint: 'https://api.flamio.org/api' });
</script>Quick start
import { Flamio } from '@flamioai/web-sdk';
Flamio.init({
projectKey: 'flm_your_project_key', // from admin.flamio.org → On-Live → Setup
endpoint: 'https://api.flamio.org/api',
version: '1.4.2', // your release version (optional but recommended)
});init() starts recording immediately and returns the instance, or null when
recording doesn't start (session not sampled, running server-side, or consent is
denied/wait). Call it once, as early as possible in your app.
Configuration
All options are passed to Flamio.init(config).
| Option | Type | Default | Description |
| ----------------------- | --------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| projectKey | string | — | Required. Your project's ingest key from the dashboard (On-Live → Setup). |
| endpoint | string | — | Required. FlamioAI ingestion URL, e.g. https://api.flamio.org/api. |
| version | string | — | Your site/release version (e.g. '1.4.2'). Attached to every session so the dashboard can filter sitemap & analytics per version and measure release impact. |
| sampleRate | number (0–1) | 1.0 | Fraction of sessions to record. 0.25 records ~25%. The decision is drawn once per visit and kept in localStorage with it, so every page of a visit agrees. |
| maxConcurrentSessions | number | 15 | Max sessions recorded at once for this project (hard-capped at 100 server-side). Beyond it, new visitors aren't recorded — a safety valve against traffic spikes. |
| consent | 'granted' \| 'denied' \| 'wait' | 'granted' | Consent gate. 'granted' records now; 'denied' records nothing; 'wait' holds until you call Flamio.consent('granted'). See Consent. |
| maskTextContent | boolean | false | Mask all visible text in the recording, not just inputs. Use for text-sensitive apps. (Input values are always masked regardless — see Privacy.) |
| maskSelectors | string[] | [] | CSS selectors whose text is blanked (same effect as the flamio-mask class, no markup changes). See Privacy. |
| blockSelectors | string[] | [] | CSS selectors whose elements are omitted from the recording (same effect as the flamio-block class). |
| urlAllowlist | string[] | [] | If non-empty, record only on URLs matching one of these patterns. * is the only wildcard; case-insensitive substring. Enforced in the browser. |
| urlBlocklist | string[] | [] | Never record on URLs matching one of these patterns (takes precedence over the allowlist). Blocked pages send nothing. |
| checkoutEveryNms | number (ms) | 30000 | How often rrweb takes a full DOM snapshot. Snapshots are seek anchors for replay and screenshots — lower = more accurate seeking, larger recordings. |
| batchInterval | number (ms) | 5000 | How often buffered events are flushed and uploaded. |
| batchMaxSize | number (chars of JSON) | 1048576 | Largest batch, in uncompressed JSON characters (≈ bytes). A buffer this full is sent early; a larger backlog goes in several batches. Was 50000 before 0.9.0. |
| idlePauseMs | number (ms) | 300000 | A visible tab with no click, scroll, typing or touch this long stops DOM recording; the next input resumes the same session with a fresh snapshot. 0 turns it off. |
| captureConsoleWarn | boolean | false | Also record console.warn (errors are always recorded). |
| retryDelayMs | number (ms) | 1000 | Base delay for retrying a failed upload (exponential backoff from here). |
| debug | boolean | false | Log SDK lifecycle events to the console. Turn off in production. |
| guide | GuideConfig | — | The in-page Guide. { launcher: true } shows the "Guide" pill; without it the guide opens only from Flamio.guide(). |
API
Flamio.init(config): Flamio | null
Starts recording. See Configuration. Returns null if the
session isn't recorded (not sampled / SSR / consent not granted). Safe to call
once; a second call is a no-op.
Flamio.identify(userId, traits?)
Attach a stable user id (and optional traits) to the current session, so the dashboard can group sessions by real user and answer "what did user X do".
Flamio.identify('user_123', { email: '[email protected]', plan: 'pro' });Flamio.consent(status)
Update consent at runtime — the counterpart to the consent: 'wait' config.
Flamio.init({ projectKey, endpoint, consent: 'wait' }); // recording deferred
// ...after the user accepts your cookie banner:
Flamio.consent('granted'); // starts recording
// or:
Flamio.consent('denied'); // stops and drops the buffer unsentFlamio.stop()
Stop recording and flush the final batch (e.g. on logout).
Flamio.guide(intent?)
Open the in-page guide, loading its chunk on first use. With an intent the run
starts straight away; without one the visitor gets the launcher and types their
own goal. Resolves to true once the guide is open — with an intent, once the
run has started — and false if it could not open. See Guide.
Flamio.stopGuide()
End the current guided run. The guide stays mounted, so the next
Flamio.guide() opens instantly.
Guide
The guide walks a visitor through a goal in their own browser: it reads the page, moves a ghost cursor to the right control, captions why, and performs the click or keystroke while the visitor watches. The first run of a goal is planned by a model, step by step; the successful path is saved as a recipe, so every later run of the same goal replays it with no planner calls, and a replay is billed nothing. A phrasing we haven't seen before still costs one small call to recognise it as the same goal — on a replay that one is on us. When the page has moved under a recipe the guide re-plans the broken stretch; those planned steps are billed, the replayed ones are not.
Flamio.init({
projectKey: 'flm_your_project_key',
endpoint: 'https://api.flamio.org/api',
version: '1.4.2', // strongly recommended — see below
guide: { launcher: true, position: 'bottom-right' },
});
// or open it from your own button, with or without a goal:
document.querySelector('#help')?.addEventListener('click', () => {
Flamio.guide('add a teammate');
});| GuideConfig | Type | Default | Description |
| ------------- | --------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------- |
| launcher | boolean | false | Show the "Guide" pill once the page goes idle. Without it the guide opens only from your code. |
| position | 'bottom-right' \| 'bottom-left' | 'bottom-right' | Which corner the pill and the step bar sit in. |
Enable it per project. The guide routes answer only for a project with the
guide feature on and a non-empty origin allowlist — the guide fails closed, so
* is refused. Ask us to turn it on for your project's domain.
One allowlist, two features. That same origin list also guards On-Live
ingest, where an empty list means "any origin". Filling it in to switch the
guide on therefore narrows recording to exactly the hosts it names: every origin
that records today has to be listed, or its batches start coming back
403 Origin not allowed for this project. Name them all in the same change that
enables the guide.
Set version. Recipes are keyed by your version, so a release that moves a
button doesn't replay a stale path: the new version starts from the recipe
carried forward from the previous one and re-verifies it. Without version all
recipes share one bucket and drift is only caught after a step fails.
Privacy markers are honoured. The page description sent to the planner obeys
the same markers as the recording: .flamio-block subtrees are omitted entirely,
.flamio-mask / data-flamio-mask / maskSelectors names become •••, and
maskTextContent: true masks every name on the page. On top of that the guide
never sends what is typed into a field — only that it is filled — and it
refuses to type into password, card, CVC/IBAN/SSN and one-time-code fields at
all: those it hands back to the visitor ("your details are needed"), which is
also what happens at checkout for name, e-mail, address and payment.
Bundle cost. The guide lives in a separate guide chunk (~29 KB gzip) that
is fetched only when it is actually used — on Flamio.guide(), on resuming a run
after a page navigation, or, with launcher: true, once the page is idle. The
UMD <script> build is a single file, so there the guide is inlined and there is
nothing to load lazily; use the ES build if the extra bytes matter.
Consent
granted(default) — record immediately.denied— record nothing: no request to Flamio, nothing written tolocalStorage, no exit handlers.wait— the same asdenieduntil you callFlamio.consent('granted'). Ideal behind a GDPR/cookie banner: callinit({ consent: 'wait' })on load, then grant/deny from the banner. The visitor id is created and recording starts only at the grant; anidentify()made before it is kept in memory and sent after it.consent('denied')after a grant stops recording and throws away whatever is still buffered without sending it. A laterconsent('granted')on the same page starts recording again.
User identification
Sessions are anonymous by default — tied to a first-party visitorId kept in
localStorage. Once you know who the user is (e.g. after login), call
Flamio.identify() to attach a real identity:
Flamio.identify('user_123', {
email: '[email protected]',
plan: 'pro',
company: 'Acme',
});userId— your stable id for the user. Lets the dashboard group all of a person's sessions and answer "what did user X do".traits— optional key/value metadata (email, plan, role, …). Shown on the session and searchable/filterable in the dashboard.
The identity travels inside the batch metadata (not as separate headers, so
any Unicode name is fine). It rides on uploads until one carrying it is accepted —
after identify(), after it changes, and at the start of every new session.
Traits whose JSON is larger than
4096 UTF-8 bytes are dropped (the userId still goes). Call identify() as soon
as the identity is known — the whole session is attributed to that user,
including events recorded before the call.
Privacy
Input values are never recorded (maskAllInputs, always on) — password,
email and phone fields are masked at the input level too.
Mark elements to skip from recordings. These conventions work identically in the SDK and the browser extension:
| Marker | Effect |
| ------------------------------------------- | ------------------------------------------------- |
| class="flamio-mask" or data-flamio-mask | Blank the element's text |
| class="flamio-block" | Omit the element entirely (replaced by a box) |
| class="flamio-ignore" | Don't record input changes inside it |
You can also drive the same rules from Flamio.init() (CSS selectors, no markup
changes needed):
Flamio.init({
projectKey: 'flm_xxx',
endpoint: 'https://api.flamio.org/api',
maskSelectors: ['.card-number', '#ssn'], // blank text
blockSelectors: ['.id-photo'], // omit element
// Control WHICH pages record (enforced in the browser — blocked pages never
// send anything). `*` is the only wildcard; matching is case-insensitive substring.
urlBlocklist: ['/admin', '/account/*'], // never record here (wins over allowlist)
urlAllowlist: ['/checkout*'], // if set, record ONLY here
});Opt into masking all page text with maskTextContent: true.
What it captures
DOM interactions (rrweb), console errors (warnings with captureConsoleWarn),
failed & slow network requests (with TTFB / transfer size / initiator when
available), Web Vitals (LCP, INP, CLS), scroll depth, SPA route changes, and tab
visibility. Console messages are cut to 2 000 characters (stacks to 4 000),
repeats of the same message within a second after it are folded into one
follow-up entry with a count, and a page records at most 100 of them.
Recording pauses on a tab hidden for 2 minutes and on a visible tab nobody
touched for idlePauseMs (5 minutes), and the session ends after 30 minutes of
inactivity. A session ends only when the page is really left (pagehide, not
entering the back/forward cache): a download link, a cancelled "Leave site?"
dialog or Back into a cached page keep it going. When the tab is hidden, what is
buffered is sent at once.
Uploads are gzipped in a small Web Worker; where a Content-Security-Policy
forbids blob: workers they are compressed on the main thread instead. With the
ES build the rrweb recorder (~23 KB gzip) is loaded only when a page actually
records — never for a visitor who is sampled out, has not consented, or is on a
blocked URL.
Wire protocol
0.9.0 speaks ingest protocol v2 (batch metadata base64url-encoded, identity in
the metadata, per-status handling of the server's answers: a refused key stops
recording for an hour, 413 splits the batch, 429/503 are retried after
Retry-After). It requires a Flamio API that includes #1177; older SDKs
keep working against that API.
Session metadata
Each session carries context collected automatically — no config or calls required. It rides on every upload until the server has stored one, so a lost first request no longer loses the session:
| Field | Source |
| ---------- | ---------------------------------------------------------------------- |
| Visitor id | first-party id in localStorage (anonymous until you identify) |
| Device | user agent, screen size, language, platform |
| Entry URL | the first page of the visit (location.href) |
| Referrer | document.referrer |
| UTM params | utm_source / medium / campaign / term / content from the URL |
| Version | your version config, when set |
Single-page apps
Set version at deploy time so the dashboard can attribute sessions to a
release. Route changes are captured automatically — no manual pageview calls.
License
MIT
