Changelog#
All notable changes to @revu-ai/core are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.2.0] - 2026-06-22#
Clean, unified identity that does not over-merge on shared devices. A family, home, library, or kiosk computer (one OS login, several people using the same web app) previously risked collapsing into a single person; sequential users on one device are now tracked separately.
Changed#
autoIdentifynow defaults tofalse(wastrue). Anonymous visitors are identified byanonymous_idalone anduser_idstaysnulluntil you callidentify(), so a non-nulluser_idalways denotes a real authenticated account. This also makes the dashboard's "identified only" filter meaningful (previously every visitor had an auto id). SetautoIdentify: trueto restore the old per-device auto id, though it is no longer recommended. Wire impact: pre-login events now shipuser_id: nullinstead of an auto UUID.reset()now rotates theanonymous_id(in addition to the session and user id). Logout severs the device thread so the next person on a shared device starts a clean identity. A returning user re-unifies by theiruser_idon the nextidentify(), so rotation does not fragment them.identify()treats a switch to a different known user as an implicit logout. When a different account logs in, the SDK emits$reset, rotates theanonymous_id, and does not stitch the two user ids together, so two accounts never merge just because they shared a device, even if the host did not callreset()on logout. An anonymous-to-identified transition still binds the existing device to the user as before.- Campaign attribution is now visitor-scoped and cleared on logout.
reset()(and the implicit logout above) now clears first-touch and last-touch attribution along with theanonymous_id, so the next person on a shared device does not inherit the previous person's acquisition campaign. The server still derives per-event campaign from the$pageviewURL; only the persisted cross-session copy is rotated.
Added#
- Debug-mode integration hint. In
debug: true, the SDK logs a one-time console hint if events flow for a while withoutidentify()ever being called, in case an app with logins forgot to wire it. Silent in production and silent onceidentify()is called; the message notes that intentionally anonymous-only sites can ignore it. - Identity integration contract documented in
docs/concepts.md(callidentify()on login,reset()on logout,alias()to join two accounts) with the shared-device guarantees spelled out per setup. - Device-id management API.
revu.getAnonymousId()returns the current device id (parity with other SDKs'getDeviceId());revu.regenerateAnonymousId()mints a fresh device id on demand, rotating only the device id and leaving the user, session, and consent intact (for an explicit device reset outside the logout flow).
Size#
- Bundle size: 33.71 kB minified / 10.58 kB gzipped (around 9 kB brotli on the wire), still under the 34 kB / 12 kB CI gate.
[0.1.0] - 2026-06-21#
First public release. Lean capture core for web behavioral analytics: one-line install, autocapture out of the box, category consent with GPC, client-side campaign attribution, a durable offline queue, persistent first-party identity, and zero runtime dependencies.
Added#
- One-line boot.
revu.init({ apiKey })wires capture, identity, consent, attribution, transport, and the queue in a single call. Every public entry (init,capture,identify,alias,reset,optOut,optIn,hasOptedOut,consent.set/consent.get,flush,use) is wrapped withsafe()so the SDK can never throw into the host page. - Autocapture.
$pageview(initial load plus SPA route changes via pushState / replaceState / popstate / hashchange),$autocapture(clicks anywhere),$rightclick,$rageclick(3 clicks on the same target within 1 s),$scrollmilestones (25 / 50 / 75 / 100%),$resize(debounced to settled value),$form_submit(field metadata only, never values),$change(control type plus checkbox / radiochecked, never values),$file_download,$outbound_link, and$page_restore(back-forward cache restore). Each element event carries a stable selector fingerprint (tag, text, role, id, classes, ordinal) plus the route path. - Engagement layer.
$page_leavewithengagement_time_ms(visible time on the page; hidden time excluded, idle time included),$tab_hidden/$tab_visiblewith paired durations,$idle/$activewith paired durations (defaultidleTimeoutMs: 30_000, off when set to0).captureAttention: falsekeeps the engagement clock but suppresses the synthetic events. - Web Vitals.
$web_vitalevents for LCP, INP, and CLS emitted on terminal page lifecycle (pagehide/ visibility-hidden). Pure PerformanceObserver, zero runtime dependencies. Disable withcaptureWebVitals: false. - Category consent + GPC. Three consent categories (
analytics,marketing,functional) viarevu.consent.set({ ... })/revu.consent.get(), withrevu.optOut()/revu.optIn()/revu.hasOptedOut()as aliases for denying / grantinganalytics. Onlyanalyticsgates capture (a denied analytics category suppresses every event before it is built);marketingandfunctionalare declarative and stamped on every event ascontext.consentfor the server to honor on downstream destinations. Global Privacy Control is stamped ascontext.gpc, andhonorGpc(default off) defaultsanalyticsto denied on a GPC signal unless the visitor has made an explicit choice. State persists in the first-party store; a legacy binary opt-out is honored on upgrade. - Campaign attribution. First touch (
context.initial_utm_*,initial_gclid,initial_fbclid, plusinitial_landing_path/initial_seen_at) is captured once and never overwritten; last touch (context.utm_*,gclid,fbclid) refreshes on each new campaign or external-referrer landing. Both persist client-side so a conversion pages or days later still carries the campaign that acquired the visitor; the server still derives a session's immediate landing from the$pageviewURL. - Persistent identity.
anonymousId(device-level) generated on first visit and persisted across reloads.userId(person-level) withautoIdentifydefault-on: a UUID is auto-generated and persisted; a laterrevu.identify("real-id")replaces it.revu.reset()rotates to a fresh auto id.sessionIdrolls forward across reloads inside a 30-minute continuation window (sessionTimeoutMs); set to0to give every page load a fresh session. An absolute cap (sessionMaxMs, default 24 h) rotates even a continuously-active session so a long-lived tab or kiosk does not accumulate one multi-day session.- Both persistent ids mirrored to
localStorageand a first-party cookie by default, so eviction of one store recovers from the other. Switch withpersistentStorage: "localStorage"to drop the cookie. cookieDomainconfig shares one visitor across subdomains.
- Identity transitions.
$identify,$reset(withprevious_user_id), and$aliasevents so the server can join the pre- and post-login behavioral graph and stitch a person across devices.revu.alias(authoritativeId)declares "the current id is the same person asauthoritativeId" without changing the local user id (motivating flow: sign up on desktop, click an email link on phone). Idempotent on the server; distinct fromidentify(), which replaces the local id. - Pipeline hooks.
beforeSend(event)runs on every built event just before it is queued: return the event (mutated or replaced) to send it,null/falseto drop it, or nothing to send it unchanged. Fail-open (a throwing hook sends the original event), and the returnedpropertiesare re-sanitized so a hook cannot poison the durable queue.autocaptureAllowSelectors/autocaptureDenySelectorsscope element-targeted autocapture by CSS selector; deny wins and suppresses the file-download / outbound-link / rage events derived from a click too. - Session sampling.
sampleRate(0-1, default1) drops whole sessions before they queue. The decision is session-sticky (a session is captured or skipped whole, never half), identity events are always sent, and kept sampled events carrycontext.sample_rateso the server can scale aggregates. - Durable transport. Batched JSON POST to
/v1/behavior/events.fetchwithkeepalive: truewhile the page is live;navigator.sendBeacononpagehide/visibilitychange=hiddento flush the last batch on unload. Capped exponential backoff on transient failures;event_idis the idempotency key so a retried batch de-dupes server-side. - Durable offline queue.
localStorage-backed buffer survives reloads and offline gaps. Auto-flushes on theonlineevent so events captured offline ship the moment connectivity returns. An unserializable event is quarantined so one bad event never blocks the queue. - First-party ingest. Point
hostat your own domain to route events first-party (data-completeness measure); reverse-proxy recipes (Cloudflare, nginx, Caddy, Next.js) are indocs/first-party-ingest.md. - Plugin contract.
revu.use(plugin)orinit({ plugins: [...] }). Plugins registered before init are queued and drained on init. The Web Vitals layer ships through this contract as a built-in plugin. - Event shape:
context+properties. Every event carries a top-levelcontextobject (engine environment, unprefixed:user_agent,language,timezone,screen_*,viewport_*,online,connection_*,environment,sdk_version,consent,gpc,sample_rate, and attribution) separate fromproperties(the event's own payload plus callercapture()props). The two buckets never collide, so there is no$-prefix and no caller-vs-engine merge - the de-facto context-vs-properties shape every warehouse / BI / SQL consumer expects.properties.pathremains the per-event path;screenis the top-level route. - SDK build version.
context.sdk_versionis stamped on every event so the server can correlate behavior with SDK versions when investigating a regression or rolling out a fix. The same string is exposed asrevu.version. Source of truth ispackage.json;src/version.jsis regenerated byscripts/sync-version.js(theprebuildhook) so a release-day bump touches one file. The generated file is committed so the vanilla example runs straight fromsrc/. - Environment label.
environment: "production" | "staging" | "development"config field (default"production") stampscontext.environmentso the dashboard can keep dev and staging traffic out of the production view. Invalid values throw at init. - Input masking and redaction. Input values are never read from any field. Click fingerprints on
<input>/<textarea>/<select>/contenteditable/[data-revu-mask]subtrees are redacted (tag, role, and selector survive; text,aria-label, andtitleare dropped);$changeskips password / file / hidden inputs entirely; form submits emit metadata only (field_names,field_types,field_count), never values. Credential and PII values in captured URLs and referrers - in both the query string and the fragment (e.g. an OAuth implicit-flow#access_token=...) - are scrubbed at source while UTM and click ids are preserved. - Shadow DOM coverage. Clicks inside open Shadow DOM custom elements are captured against the actual internal element via
composedPath()instead of the retargeted host. Ancestor walks in the fingerprint (selector path) and masking check (data-revu-mask) cross the shadow boundary viaShadowRoot.host, so a mask on the host applies to its shadow tree and selectors keep their full ancestor context across Web Components and component-library UIs.
Architecture#
- Vanilla JavaScript (ESM) with JSDoc as the single source of truth for both runtime and emitted
.d.tstypes. - Zero runtime dependencies. No published package declares a
dependenciesentry. Platform and Web APIs only. - Tree-shakeable.
"sideEffects": false; unused modules drop out at the consumer's bundler. - Bundle size: 32.41 kB minified / 10.22 kB gzipped (around 9 kB brotli on the wire), under the 34 kB / 12 kB CI gate.
- Defensive boundary. Every public entry is
safe()-wrapped; internal errors are swallowed and (indebug: true) logged, never propagated to the host page.
Security#
- Input values are never read from any field (
<input>,<textarea>,<select>,contenteditable) - only interactions and structure. - Form submit events carry shape metadata only, never values.
data-revu-maskopts a subtree out of text and label capture;autocaptureDenySelectorssuppresses capture for a region entirely.- Credential and PII values in captured URLs and referrers are scrubbed at source - in both the query string and the fragment (UTM and click ids preserved).
- Consent is enforced before an event is built (a denied
analyticscategory produces no event); Global Privacy Control is honored whenhonorGpcis set. - The transport sends only fields explicitly built by the client; no DOM serialization, no cookie reads other than the SDK's own first-party identity cookie.
[0.2.0]: https://github.com/revu-ai/sdk-web/releases/tag/v0.2.0 [0.1.0]: https://github.com/revu-ai/sdk-web/releases/tag/v0.1.0