Events & Analytics
RevTurbine reports what it did — a placement was shown, a CTA was clicked — and, when you opt in, what the user was doing around it. Placement lifecycle is tracked automatically; everything else is opt-in, additive, and privacy-safe by default. An integration that sets no telemetry options behaves exactly as before.
Every event carries a sortable, unique event_id, is PII-redacted before it leaves the browser, and is batched and delivered best-effort (a failed send never throws into your app, and a retry resends the identical row so it can’t duplicate).
Automatic tracking
Section titled “Automatic tracking”Placement components and headless controllers instrument the placement lifecycle for you — you don’t wire these up:
| Event | When |
|---|---|
impression | A placement is presented (see viewport exposure) |
placement_interaction | The canonical interaction event, discriminated by interaction_type: dismiss · remind_me_later · cta_clicked · cta_completed · suppress |
placement_rendered / placement_exposed | The visual root rendered / entered the viewport (when you attach exposureRef) |
placement_interaction is the one placement interaction event; there are no separate dismiss/snooze/convert events.
Billing facts and application events
Section titled “Billing facts and application events”With Stripe connected and identity mapping configured, RevTurbine records subscription, trial, renewal, and payment facts from Stripe webhooks. Your app does not need to emit a second billing fact merely because a checkout page returned successfully. A browser return URL is not authoritative payment proof.
The billing event names are subscription_started, subscription_renewed,
subscription_expanded, subscription_canceled, trial_started,
trial_converted, trial_expired, payment_succeeded, and payment_failed.
For example, record your app’s checkout-button interaction as an application
event; let the webhook integration report the resulting payment_succeeded
and subscription fact. Do not send a duplicate subscription_started from
the browser’s checkout-success page.
Use application events for what your app knows: feature use, an onboarding step,
or an interaction that is not a Stripe billing occurrence. track() is the
generic lane and namespaces collisions with platform event names. The typed
emitPlatformEvent API is the platform-event lane and requires that event’s
payload contract; renaming an arbitrary track() event does not make it a
canonical billing fact.
billing_ref is a producer-owned stable billing-fact identity, not the SDK
transport event_id. Preserve it when retrying the same fact. Do not reuse it
for unrelated occurrences or invent a new one on each retry. Newer managed
billing profiles distinguish occurrence identity, source revision, delivery ID,
and the legacy billing reference; a legacy reference alone does not identify
every repeated subscription transition. Prefer the Stripe integration for
Stripe-owned facts rather than reproducing its accounting logic in the browser.
The SDK does not emit the removed growth_signal_observed event. Use the current
documented event/trigger contracts and keep custom names in the application lane.
For identity mapping and webhook setup, see Billing & Stripe.
Custom events
Section titled “Custom events”In React, useTrack returns a track(name, data?, options?) bound to the surrounding scope. Reserved names (canonical identity + provenance fields) are dropped so you can’t overwrite a system value; without a provider, track is a safe no-op.
import { useTrack } from '@revturbine/sdk';
function AdvancedFilters() { const track = useTrack(); return ( <button onClick={() => track('feature_used', { feature: 'advanced_filters' })}> Filter </button> );}Headless / server code uses sdk.track:
await sdk.track('checkout_completed', { plan: 'professional', revenue: 99 });Track options
Section titled “Track options”| Option | Meaning |
|---|---|
area / action | Scope labels; merge outer → inner → invocation |
purpose | Advisory intent (e.g. 'engagement'). A server-side allowlist — not this value — decides whether an event feeds scoring |
once | Emit at most once for the hook’s lifetime |
dedupeKey | Emit at most once per key |
immediate | Send now instead of batching |
Scoping with TelemetryScope
Section titled “Scoping with TelemetryScope”TelemetryScope sets ambient area / action / purpose for its descendants. It renders its children unchanged — no wrapper element, no added role — and nested scopes merge inner-over-outer, with per-event options winning.
import { TelemetryScope, useTrack } from '@revturbine/sdk';
<TelemetryScope area="billing" purpose="engagement"> <UpgradePanel /> {/* every useTrack() here inherits area="billing" */}</TelemetryScope>Declarative tracking
Section titled “Declarative tracking”| Component / hook | Fires |
|---|---|
TrackOnView | Its event once when the element scrolls into view (falls back to render when IntersectionObserver is unavailable). Strict-Mode safe |
EngagementArea | A one-shot engagement_view, accrues engagement_dwell (dwell_ms) only while onscreen and the tab is visible, and bubbles descendant clicks as engagement_interaction |
Track (asChild) | Composes onto a child’s onClick (no wrapper) — telemetry fires only if the child didn’t preventDefault; leaves the child’s accessible name and disabled state unchanged |
useTelemetryProps | Telemetry props to spread onto a primitive that can’t take a wrapper |
import { TrackOnView, Track } from '@revturbine/sdk';
export const Examples = () => ( <> <TrackOnView event="hero_seen" data={{ variant: 'a' }}> <Hero /> </TrackOnView>
<Track event="cta_clicked" data={{ plan: 'pro' }} asChild> <button onClick={handleUpgrade}>Upgrade</button> </Track> </>);Tracked & gated actions
Section titled “Tracked & gated actions”useTrackedAction wraps an async action, emitting ${name}_started then ${name}_completed or ${name}_failed (with a non-sensitive error_category — never the raw message). It preserves the return value and re-throws, so it’s a drop-in wrapper.
import { useTrackedAction } from '@revturbine/sdk';
const { run, isRunning } = useTrackedAction('export_pdf', () => exportPdf());<button disabled={isRunning} onClick={() => run()}>Export</button>useGatedAction is the React analog of rt.gate(action, fn): it delegates to the SDK so it emits the same gate_attempted → gate_allowed / gate_denied sequence (not a fork), running the action wrapped in tracked-action telemetry only when allowed. A passively-rendered <Gate> instead emits gate_evaluated.
Consent
Section titled “Consent”telemetry.consent gates event creation ahead of every destination:
| Value | Effect |
|---|---|
granted (default) | Emit normally |
denied | No event is created — nothing reaches ingest, consumers, or integrations |
pending | Dropped in MVP; never persisted |
Change it at runtime with no remount:
sdk.setTelemetryConsent('denied'); // stop; 'granted' resumes on the next eventThe keyless anonymous SDK-init beacon (anonymousTelemetry) is controlled separately and is unaffected by consent.
Annotated DOM capture
Section titled “Annotated DOM capture”Opt into hand-authored capture with domCapture on the provider. One delegated listener per event reads only allowlisted data-rt-* attributes — never element text, input values, hrefs, or selectors — and always skips password / file / hidden / payment-autocomplete controls. A data-rt-no-capture ancestor opts an element and its subtree out. Collected values pass through the same PII redactor.
<RevTurbineProvider options={options} domCapture> <button data-rt-event="cta_clicked" data-rt-prop-plan="pro">Upgrade</button></RevTurbineProvider>Viewport exposure
Section titled “Viewport exposure”placementExposure controls when the presentation-writing impression fires:
| Mode | Impression fires |
|---|---|
legacy_resolution (default) | At decision resolution — exactly as today |
render | When the placement renders |
viewport | When it scrolls into the viewport; falls back to resolution (exposure_basis: 'render_fallback') when IntersectionObserver is unavailable |
usePlacement returns exposureRef; slot components receive the same ref through the additive exposureRef prop on PlacementSlotProps. The renderer threads it to every slot automatically, and a custom component must attach it to its true visual root. The built-in slots do not consume the ref, so their viewport mode uses the render fallback.
Third-party analytics
Section titled “Third-party analytics”Forward the same semantic events to a third-party tool without adding a second transport. createPostHogIntegration adds an opt-in identity lifecycle (all sync flags default false, so it behaves like the capture-only provider until you turn one on); PostHog is injected, never bundled.
import posthog from 'posthog-js';import { createPostHogIntegration } from '@revturbine/sdk';
initRevTurbine({ domainProviders: [createPostHogIntegration({ posthog, syncIdentity: true })], // ...});A throwing integration never blocks RevTurbine ingest, and an ingest failure never blocks a configured mirror.
Trigger events
Section titled “Trigger events”Trigger events are semantic lifecycle signals the SDK uses to re-evaluate targeting. Emit one and all active placements re-evaluate:
await sdk.emitTrigger('trial_expiring', { days_remaining: 2 });await sdk.emitTrigger('usage_limit_approaching', { entitlement_handle: 'api_calls', usage_percent: 95,});Built-in triggers: trial_midpoint · trial_expiring · trial_expired · usage_limit_approaching · usage_limit_reached · credit_balance_low · seat_limit_reached · feature_gated · cancel_intent · payment_failed · auto_renewal_reminder · onboarding_complete · invite_teammate_prompt · referral_offer · plan_upgrade_nudge. The compiler rejects any other name.
Marking test traffic
Section titled “Marking test traffic”Set test: true at init when an SDK instance generates test traffic — a staging deploy, an integration test, a QA walkthrough:
import { initRevTurbine } from '@revturbine/sdk';
const rt = await initRevTurbine({ publicKey: process.env.NEXT_PUBLIC_REVTURBINE_PUBLIC_KEY!, // public key — safe in the browser test: true, // this instance's events are test traffic});Every emitted event (clickstream and treatment interactions) is then stamped test: true, and RevTurbine analytics exclude those events by default from every metric, rollup, and denominator. An include_test=1 toggle on the analytics pipes reveals them when you want to inspect test traffic.
This is a deliberate, code-passed decision: pass it explicitly from your own configuration. The SDK never infers it from the environment — no NODE_ENV sniffing. Whether traffic is test traffic is your call, not a deployment artifact. Omitted or false, events are emitted unchanged (byte-identical to earlier SDK versions).
Silent-failure diagnostics
Section titled “Silent-failure diagnostics”When a placement or entitlement can’t resolve for infrastructure reasons, the SDK reports it instead of failing silently — a resolution_failure diagnostic on the anonymous meta channel:
| Reason | Emitted when |
|---|---|
placement_not_registered | A decision is requested for a placement the app never registered — the classic “my payload renders nothing, silently” |
config_unavailable | The Playbook never arrived, so a placement fell back or an entitlement check denied without a rule saying no |
sdk_disabled_provider_failure | The provider chain failed: entitlement checks deny with this reason and slots follow providerFailureSlotBehavior |
Diagnostics carry only author-defined handles (placement, slot, plan, entitlement) and closed reason codes — never user context, free text, or your tenant identifier (deployments are counted through a one-way hash). They’re deduplicated per session and capped, fire from keyed production installs as well as keyless ones, and are silenced by either opt-out:
initRevTurbine({ analytics: false }); // silences diagnostics tooinitRevTurbine({ anonymousTelemetry: false }); // likewiseRule-based denials and local mode’s default behavior are not failures and never emit.
Your taxonomy in analytics
Section titled “Your taxonomy in analytics”Events that carry the area / action / purpose taxonomy (from TelemetryScope, useTrack options, EngagementArea, and tracked actions) are additionally stored with the taxonomy as first-class analytics columns — so engagement queries group by area and action directly, with no JSON unpacking and no configuration on your side. Untagged events are unaffected.
Privacy & delivery
Section titled “Privacy & delivery”- Redaction. Email- and card-shaped values in properties and traits are scrubbed to
[REDACTED]before any destination — one sanitized envelope, every mirror. Best-effort, not a guarantee: don’t put PII in event data. - Advisory purpose, server-owned scoring. A client-declared
purposenever grants engagement eligibility; a static server-side allowlist decides which event names feed scoring, regardless of purpose. - Delivery. Events batch and flush on size, interval, or page-unload. A transient failure is retried with the byte-identical row, so storage collapses it to one; a permanent failure is dropped silently. In
local_onlymode nothing is sent at all — the right mode for demos and examples.
Next steps
Section titled “Next steps”- Placements Guide — placement lifecycle and interactions
- Runtime Modes — local vs hosted mode
- Error Handling — graceful event-delivery failures