Initialize the Reveal SDK. Call this once at app startup.
Reveal.init(clientKey: string, options?: InitOptions): Promise<void>Parameters:
clientKey(string, required) - Your Reveal client key (identifies your project)options(object, optional) - Configuration options
Init Options:
| Option | Type | Default | Who Sets It? | Description |
|---|---|---|---|---|
clientKey |
string |
- | You (required) | Your project's client key from Reveal dashboard |
apiBase |
string |
Auto-resolved from environment |
You (if self-hosting) | Backend API base URL. Used to construct config, ingest, and decision endpoints. If not provided, SDK automatically resolves from environment parameter: production → https://api.revealos.com, staging → https://api-staging.revealos.com, development → http://localhost:3000. Only needed if self-hosting or using non-default URL |
configEndpoint |
string |
"{apiBase}/config" |
You (if custom) | Explicit config endpoint. Overrides apiBase for config fetch. SDK fetches client-safe configuration from this endpoint during initialization |
ingestEndpoint |
string |
"{apiBase}/ingest" |
You (if custom) | Explicit event ingestion endpoint. Overrides apiBase |
decisionEndpoint |
string |
"{apiBase}/decide" |
You (if custom) | Explicit decision endpoint. Overrides apiBase. Note: If backend config returns a relative path (e.g., /decide), SDK automatically resolves it using apiBase |
decisionTimeoutMs |
number |
1500 (production/staging), 2000 (development) |
You (if custom) | Timeout for decision requests in milliseconds. Defaults are environment-aware: 1500ms for production/staging (realistic for network + backend processing, avoids false negatives), 2000ms for development (allows for CORS preflight + logging overhead) |
debug |
boolean |
false |
You (dev only) | Enable debug logging. Set to true in development |
environment |
string |
"development" |
You | Environment: "production" | "staging" | "development". Controls two things: (1) which engine host is called (via auto-resolved apiBase), and (2) which environment value is sent to backend as query param for data isolation. If apiBase is not provided, SDK automatically resolves it based on environment: production → https://api.revealos.com, staging → https://api-staging.revealos.com, development → http://localhost:3000 |
Security Note: All backend URLs (configEndpoint, ingestEndpoint, decisionEndpoint, apiBase) must use HTTPS protocol. The SDK will disable itself at initialization if any non-HTTPS URL is detected. Exception: http://localhost and http://127.0.0.1 are allowed for local development only.
Config Fetch Behavior: During initialization, the SDK attempts to fetch configuration from the backend /config endpoint. If the fetch succeeds, the SDK uses the backend config (including decision.endpoint which may be a relative path like /decide). If the fetch fails, the SDK gracefully falls back to a minimalConfig constructed from initialization options, ensuring backward compatibility. Relative decision endpoints from backend config are automatically resolved to full URLs using apiBase before validation.
Note: The backend decides which nudges to show, but the SDK still needs to know where to send events and requests. In production apps, you typically only set clientKey (and optionally apiBase if self-hosting). The harness app sets all options because it mocks backend endpoints for local testing.
Examples:
// Minimal setup (most common)
await Reveal.init('proj_abc123');
// With custom API base (self-hosting)
await Reveal.init('proj_abc123', {
apiBase: 'https://your-api.example.com',
});
// Development setup with debug logging
await Reveal.init('proj_abc123', {
debug: process.env.NODE_ENV === 'development',
environment: 'development',
});
// Staging setup (auto-resolves to api-staging.revealos.com)
await Reveal.init('proj_abc123', {
environment: 'staging',
});You must explicitly choose between production and staging environments. This is a critical decision that affects data isolation and where your events are stored.
Production (environment: 'production'):
- Use for: Live applications serving real users
- API endpoint:
https://api.revealos.com - Data storage: Production database (separate from staging)
- Impact: Events appear in production dashboards and affect production analytics
Staging (environment: 'staging'):
- Use for: Testing, QA, pre-production validation
- API endpoint:
https://api-staging.revealos.com - Data storage: Staging database (completely isolated from production)
- Impact: Events appear only in staging dashboards, never in production
Why this matters:
- Data isolation: Staging and production use separate databases. Events sent to staging never appear in production dashboards, and vice versa.
- Client key requirement: You need a separate staging client key from your Reveal dashboard. Production and staging client keys are different.
- Testing safety: Using staging for testing prevents test events from polluting production analytics and ensures your production nudge decisions aren't affected by test data.
For development projects, you can override the environment via the X-Reveal-Environment header when sending events to the /ingest endpoint. This is useful for testing different environments without changing the project configuration.
Important Restrictions:
- Only works for development projects: Projects with
environment: "development"can use this feature - Production/staging projects are protected: Override is ignored for production and staging projects (security measure)
- Valid values:
"development","staging", or"production"(case-insensitive) - Invalid values are ignored: If an invalid environment is provided, the project's configured environment is used
Usage Example:
# Development project with override to staging
curl -X POST https://api.revealos.com/ingest \
-H "X-Reveal-Client-Key: dev-project-key" \
-H "X-Reveal-Environment: staging" \
-H "Content-Type: application/json" \
-d '{
"events": [{
"event_id": "evt_123",
"session_id": "sess_456",
"timestamp": "2024-01-01T00:00:00Z",
"event_kind": "product",
"event_type": "button_clicked",
"anonymous_id": "anon_789"
}]
}'Behavior:
- If project is
developmentand header isstaging: Events are stored withenvironment: "staging" - If project is
productionand header isstaging: Override is ignored, events stored withenvironment: "production" - If project is
developmentand header is invalid: Override is ignored, events stored withenvironment: "development"
Environment Mismatch Warnings:
If the client sends events with an environment field that doesn't match the project's configured environment (or override), the backend will:
- Log a warning with details about the mismatch
- Use the project's configured environment (or override) for data storage
- Continue processing the event normally
This ensures data consistency while alerting you to potential misconfigurations.
Recommendation: Always use environment: 'staging' with a staging client key for testing and development. Only use environment: 'production' when deploying to production with your production client key.
Default behavior: If you don't specify environment, it defaults to "development" (localhost). This is only suitable for local development with a local engine server.
During initialization, the SDK fetches configuration from the backend /config endpoint. This config controls SDK behavior and treatment assignment.
Config Fields:
| Field | Type | Description |
|---|---|---|
configVersion |
number |
Config schema version (currently 1) |
projectId |
string |
Project identifier |
environment |
string |
Environment: "production" | "staging" | "development" |
sdk.samplingRate |
number |
Event sampling rate (0.0 to 1.0). Controls which users send events to /ingest. Note: Sampling only affects /ingest events, NOT /decide requests. All users can receive nudges regardless of sampling. |
features.enabled |
boolean |
Whether SDK features are enabled |
features.detectors |
object |
Friction detector enable flags: { stall, rageclick, backtrack } |
features.nudges |
object |
Nudge template enable flags: { tooltip, modal, banner, spotlight, inline_hint } |
treatment_rules |
object | undefined |
Treatment assignment rules (A/B testing). If undefined, no treatment assignment occurs. |
treatment_rules.sticky |
boolean |
Whether treatment is sticky (based on anonymousId) or per-session (based on sessionId). Default: true |
treatment_rules.treatment_percentage |
number |
Percentage of users assigned to treatment group (0-100). Uses hash-mod-100 bucketing. |
decision.endpoint |
string |
Decision endpoint path (may be relative like /decide) |
decision.timeoutMs |
number |
Decision timeout in milliseconds |
progress_timeout_rules |
object | undefined |
Progress timeout detector configuration (optional, disabled by default) |
progress_timeout_rules.enabled |
boolean |
Whether progress timeout detector is enabled. Default: false |
progress_timeout_rules.timeout_seconds |
number |
Timeout threshold in seconds. Detector emits friction_no_progress if no progress events occur within this duration |
progress_timeout_rules.hard_timeout_seconds |
number | undefined |
Optional hard timeout threshold for higher confidence detection |
progress_timeout_rules.progress_event_names |
string[] |
Array of product event names that count as progress (e.g., ["card_created", "card_updated"]). Detector monitors these events and resets timer when they occur |
templates |
array |
Nudge templates (empty array for client config - templates are backend-only) |
ttlSeconds |
number |
Config cache TTL in seconds |
Progress Timeout Detector:
- Feature is disabled by default (
enabled: false) - When enabled, detector monitors product events matching
progress_event_names - Timer starts from detector initialization (or last matching progress event)
- If no matching progress events occur within
timeout_seconds, emitsfriction_no_progresssignal - Progress events reset the timer, preventing false positives during active usage
- Supports optional
hard_timeout_secondsfor higher confidence detection (e.g., for critical flows) - Detector observes events via EventPipeline callback (no direct DOM access, no network calls)
Sampling Behavior:
samplingRate: 1.0→ All users send events to/ingest(100% sampling)samplingRate: 0.5→ 50% of users send events (deterministic hash-based bucketing)samplingRate: 0.0→ No users send events to/ingest(0% sampling)- Sampling decision is computed at init and persisted to localStorage:
reveal_sampled_in_{projectId}_{anonymousId} - Important: Sampling does NOT affect
/deciderequests - all users can receive nudges
Treatment Assignment:
- If
treatment_rulesexists, users are assigned to"treatment"or"control"cohort at init - Treatment is computed using hash-mod-100 bucketing on
anonymousId(sticky) orsessionId(non-sticky) - Treatment is persisted to localStorage:
reveal_treatment_{projectId}_{anonymousId} - If localStorage fails, treatment still works but won't persist across page reloads (fail-open behavior)
localStorage Keys:
reveal_treatment_{projectId}_{anonymousId}- Treatment assignment (best-effort persistence)reveal_sampled_in_{projectId}_{anonymousId}- Sampling decision (best-effort persistence)- Both keys are scoped by
projectIdandanonymousIdto prevent cross-project contamination - SDK handles localStorage failures gracefully (SafeTry protection)
Example Config:
{
"configVersion": 1,
"projectId": "proj_abc123",
"environment": "production",
"sdk": {
"samplingRate": 0.5
},
"features": {
"enabled": true,
"detectors": {
"stall": true,
"rageclick": true,
"backtrack": true
},
"nudges": {
"tooltip": true,
"modal": true,
"banner": false,
"spotlight": true,
"inline_hint": true
}
},
"treatment_rules": {
"sticky": true,
"treatment_percentage": 50
},
"decision": {
"endpoint": "/decide",
"timeoutMs": 1500
},
"progress_timeout_rules": {
"enabled": true,
"timeout_seconds": 60,
"hard_timeout_seconds": 180,
"progress_event_names": ["card_created", "card_updated"]
},
"templates": [],
"ttlSeconds": 300
}Track an event.
Reveal.track(
eventKind: EventKind,
eventType: string,
properties?: EventPayload
): voidNote: Events are automatically transformed from SDK internal format (BaseEvent) to backend format (EventModelContract.Event) before sending to the /ingest endpoint. This transformation includes:
- Field name mapping (
kind→event_kind,name→event_type) - Timestamp conversion (number → ISO 8601 string)
- Addition of required fields (
event_id,anonymous_id,sdk_version) - Page context extraction (
page_url,page_title,referrer) - Friction event special handling (extracts
selector,page_url,friction_typefrom payload)
The SDK API (Reveal.track()) remains unchanged - transformation is handled internally.
Examples:
// Product event with payload and semantic IDs
Reveal.track('product', 'checkout_started', {
action_id: 'checkout_started',
flow_id: 'purchase',
step: 1,
cartValue: 99.99,
itemCount: 3,
currency: 'USD',
hasDiscount: true,
});
// Friction event with payload
Reveal.track('friction', 'stall_detected', {
stallDurationMs: 20000,
pageUrl: '/checkout',
selector: '#submit-button',
});
// Nudge event with payload
Reveal.track('nudge', 'nudge_clicked', {
nudgeId: 'nudge_123',
templateId: 'tooltip',
action: 'cta_clicked',
});
// Event without payload
Reveal.track('product', 'page_viewed');There are two paths for handling nudge decisions, depending on your framework:
Use the useNudgeDecision hook for the simplest integration:
import { useNudgeDecision } from '@reveal/client';
import { OverlayManager } from '@reveal/overlay-react';
function App() {
const { decision, handlers } = useNudgeDecision();
return (
<>
{/* Your app content */}
<OverlayManager
decision={decision}
onDismiss={handlers.onDismiss}
onActionClick={handlers.onActionClick}
onTrack={handlers.onTrack}
/>
</>
);
}Why this path? The hook automatically:
- Subscribes to nudge decisions
- Converts wire format to UI format
- Provides tracking handlers
- Handles cleanup on unmount
Use Reveal.onNudgeDecision for vanilla JS, Vue, Angular, or custom implementations:
import { Reveal } from '@reveal/client';
const unsubscribe = Reveal.onNudgeDecision((decision) => {
// decision is a WireNudgeDecision from backend
// Render the nudge using your UI framework
renderNudge(decision);
});
// Later, to unsubscribe:
unsubscribe();Note: In this path, you're responsible for:
- Converting
WireNudgeDecisionto your UI format (if needed) - Rendering the nudge in your UI
- Tracking nudge interactions
- Managing subscription lifecycle
Subscribe to nudge decisions from the backend. Use this for framework-agnostic apps (vanilla JS, Vue, Angular, etc.).
Reveal.onNudgeDecision(
handler: (decision: WireNudgeDecision) => void
): () => voidParameters:
handler(function) - Callback that receivesWireNudgeDecisionobjects
Returns: Unsubscribe function
When to use:
- Vanilla JavaScript apps
- Vue, Angular, or other non-React frameworks
- Custom UI implementations
- When you need full control over nudge rendering
Example:
const unsubscribe = Reveal.onNudgeDecision((decision) => {
// decision is a WireNudgeDecision from backend
if (decision.templateId === 'tooltip') {
showTooltip(decision);
} else if (decision.templateId === 'modal') {
showModal(decision);
}
});
// Later, to unsubscribe:
unsubscribe();Note: If you're using React, prefer useNudgeDecision() hook instead.
Recommended for React apps. React hook that subscribes to nudge decisions and provides UI-ready decision state with tracking handlers.
Requirements:
- React >= 18.0.0 (peer dependency)
@reveal/overlay-reactpackage (forOverlayManagercomponent)
Returns: Object with:
decision(UINudgeDecision | null) - Current nudge decision in UI format (automatically converted from wire format)handlers- Object containing:onDismiss- Handler for nudge dismissal (automatically tracksnudge_dismissedevent)onActionClick- Handler for nudge action/CTA clicks (automatically tracksnudge_clickedevent)onTrack- Handler for tracking custom events
What it does automatically:
- ✅ Subscribes to
Reveal.onNudgeDecisionon mount - ✅ Converts
WireNudgeDecisiontoUINudgeDecisionusingmapWireToUI - ✅ Unsubscribes on unmount
- ✅ Provides tracking handlers that call
Reveal.trackinternally
Example:
import { useNudgeDecision } from '@reveal/client';
import { OverlayManager } from '@reveal/overlay-react';
function App() {
const { decision, handlers } = useNudgeDecision();
return (
<>
{/* Your app content */}
<OverlayManager
decision={decision}
{...handlers}
/>
</>
);
}- EventKind:
"product" | "friction" | "nudge" | "session" - EventPayload:
Record<string, any>- Event-specific properties (flat object with primitive values) - FrictionSignal: Friction detection signal emitted by detectors
Understanding the naming conventions helps when working with nudges:
- Nudge - A contextual message/UI element shown to guide users (tooltip, modal, banner, etc.)
- WireNudgeDecision - Raw decision format from backend (canonical wire protocol between SDK and backend)
- UINudgeDecision - UI-ready decision format (mapped from wire format, includes computed fields like
severity) - NudgeDecision - Type alias for
UINudgeDecision(the UI-facing type you'll use) - Template - Pre-built nudge UI component (tooltip, modal, banner, spotlight, inline_hint)
- TemplateId - Identifier for template type:
"tooltip" | "modal" | "banner" | "spotlight" | "inline_hint" - OverlayManager - React component that renders the appropriate template based on decision
- useNudgeDecision - React hook that manages nudge subscription and provides UI-ready state
- Quadrant Positioning - Overlay positioning strategy using 6 viewport quadrants (topLeft, topCenter, topRight, bottomLeft, bottomCenter, bottomRight)
- Replaces target element attachment approach for better flexibility
- Backend can specify quadrant preference via
WireNudgeDecision.quadrantfield - Defaults to
"topCenter"if not specified - Prevents overlays from blocking critical UI elements
- selectorPattern - CSS selector for spotlight template target element
- Backend config uses
selector_pattern(snake_case) in template config - Wire protocol uses
selectorPattern(camelCase) inWireNudgeDecisionandUINudgeDecision - Spotlight template uses this selector to query DOM for target element to highlight
- If selector not found, spotlight dismisses with reason
"target_not_found" - Quadrant templates (tooltip, inline_hint) do not use selectorPattern for positioning
- Backend config uses
Flow:
- Backend sends
WireNudgeDecision→ SDK receives it - SDK converts to
UINudgeDecision(viamapWireToUI) → UI-ready format OverlayManagerrenders appropriate template → User sees nudge
- WireNudgeDecision - Wire-level decision format (from backend, canonical protocol)
- UINudgeDecision - UI-facing decision format (for React components, includes computed fields)
- NudgeDecision - Type alias for
UINudgeDecision(the UI-facing type) - NudgeTemplateId - Template identifier union type:
"tooltip" | "modal" | "banner" | "spotlight" | "inline_hint" - NudgeSeverity - Severity level union type
- mapWireToUI() - Function to convert
WireNudgeDecisiontoUINudgeDecision
Event payload type for event-specific properties.
Type Definition:
type EventPayload = Record<string, any>;Constraints:
- Flat object structure (no nested objects or arrays)
- Values must be primitives:
string | number | boolean | null - Must be JSON-serializable
- Recommended max size: 10KB
Semantic IDs (Recommended for Product Events):
For product events, we recommend including semantic identifiers to enable better analytics and targeting:
action_idorfeature_id(string, required) - Stable identifier for the action or feature (e.g.,"create_project_click","signup_button_click")flow_id(string, optional) - Identifier for the user flow or journey (e.g.,"onboarding","checkout","purchase")step(string | number, optional) - Step number or identifier within a flow (e.g.,1,"step_2","payment")success(boolean, required for submits/checkout/completion events) - Whether the action succeeded or failed
Valid Examples:
// Product event payload with semantic IDs
Reveal.track('product', 'checkout_started', {
action_id: 'checkout_started',
flow_id: 'purchase',
step: 1,
cartValue: 99.99,
itemCount: 3,
currency: 'USD',
hasDiscount: true,
userId: 'user_123',
timestamp: null, // null values are allowed
});
// Form submission with success indicator
Reveal.track('product', 'form_submitted', {
action_id: 'signup_form_submit',
flow_id: 'onboarding',
step: 2,
success: true,
formId: 'signup',
});
// Product event without semantic IDs (still valid)
Reveal.track('product', 'checkout_started', {
cartValue: 99.99,
itemCount: 3,
currency: 'USD',
hasDiscount: true,
});
// Friction event payload
Reveal.track('friction', 'stall_detected', {
stallDurationMs: 20000,
pageUrl: '/checkout',
selector: '#submit-button',
context: 'checkout_form',
});
// Nudge event payload
Reveal.track('nudge', 'nudge_clicked', {
nudgeId: 'nudge_123',
templateId: 'tooltip',
action: 'cta_clicked',
frictionType: 'stall',
});Invalid Examples:
// ❌ Nested objects not allowed
Reveal.track('product', 'event', {
user: { id: '123', name: 'John' } // Invalid: nested object
});
// ❌ Arrays not allowed
Reveal.track('product', 'event', {
items: ['item1', 'item2'] // Invalid: array value
});
// ❌ Functions not allowed
Reveal.track('product', 'event', {
callback: () => {} // Invalid: function value
});
// ❌ Dates must be converted to strings or numbers
Reveal.track('product', 'event', {
createdAt: new Date() // Invalid: Date object
// ✅ Valid: createdAt: Date.now() or createdAt: new Date().toISOString()
});See src/types/ for complete type definitions.