@spanwise/rum
v0.8.0
Published
Real User Monitoring SDK for Spanwise - track sessions, page views, clicks, and Core Web Vitals
Downloads
38
Maintainers
Readme
@spanwise/rum
Real User Monitoring SDK for browser applications. Captures page views, clicks, errors, Web Vitals, and session replays.
Installation
npm install @spanwise/rum
# or
pnpm add @spanwise/rum
# or
yarn add @spanwise/rumQuick Start
import { createSpanwiseBrowser } from "@spanwise/rum"
const spanwise = createSpanwiseBrowser({
apiKey: "sw_...",
serviceName: "my-app",
})
// That's it! Page views, clicks, errors, and vitals are tracked automatically.Configuration
const spanwise = createSpanwiseBrowser({
// Required
apiKey: "sw_...",
// Optional
serviceName: "my-app", // Service name for filtering
sampleRate: 1.0, // Sample 100% of sessions (default: 1.0)
trackPageViews: true, // Track page views (default: true)
trackClicks: true, // Track clicks (default: true)
trackVitals: true, // Track Web Vitals (default: true)
trackErrors: true, // Track errors (default: true)
trackResources: true, // Track fetch/XHR requests (default: true)
sessionStorage: "cookie", // "cookie" or "sessionStorage" (default: "cookie")
// Distributed Tracing
tracing: {
enabled: true, // Inject traceparent headers (default: true)
propagateToOrigins: [ // Origins to propagate trace context to
"https://api.example.com",
/\.example\.com$/,
],
},
// Session Replay (opt-in)
replay: {
enabled: true, // Enable replay recording (default: false)
sampleRate: 0.1, // Record 10% of sessions (default: 0.1)
onErrorSampleRate: 1.0, // Record 100% of sessions with errors (default: 1.0)
privacyMode: "strict", // Privacy mode (default: "strict")
},
})API
spanwise.trackEvent(name, properties?)
Track a custom event.
spanwise.trackEvent("purchase", {
productId: "123",
amount: 99.99,
})spanwise.trackPageView(url?)
Manually track a page view. Called automatically on navigation.
spanwise.trackPageView("/checkout")spanwise.trackError(error, options?)
Manually track an error.
try {
await riskyOperation()
} catch (error) {
spanwise.trackError(error, {
requestId: "req_123",
traceId: "trace_abc",
})
}spanwise.setUser(id, traits?)
Identify the current user.
spanwise.setUser("user_123", {
email: "[email protected]",
plan: "pro",
})spanwise.getSessionId()
Get the current session ID.
const sessionId = spanwise.getSessionId()spanwise.forceReplayUpload()
Force replay upload for specific users (bypasses sampling).
// Force upload for VIP users regardless of sample rate
if (user.isVIP) {
spanwise.forceReplayUpload()
}spanwise.stopReplay()
Stop replay recording entirely.
spanwise.isReplayUploading()
Check if replay is currently uploading to server (vs buffering in memory).
Session Replay
Session Replay records user interactions as a video-like playback. It captures DOM mutations, mouse movements, clicks, and scrolls.
Enabling Replay
const spanwise = createSpanwiseBrowser({
apiKey: "sw_...",
replay: {
enabled: true,
},
})How It Works: Rolling Buffer
Session Replay uses a rolling buffer architecture (similar to Sentry):
- Always recording - When enabled, the SDK always records to a 60-second memory buffer
- Sampled sessions (default 10%) - Upload immediately to server
- Non-sampled sessions (90%) - Keep buffer in memory, zero network cost
- On error - Flush the 60-second buffer and start uploading
This means you always capture what happened before an error, not just after.
Sampling
replay: {
enabled: true,
sampleRate: 0.1, // 10% upload immediately
onErrorSampleRate: 1.0, // 100% upload on error (includes 60s buffer)
}| Scenario | What's Captured | |----------|-----------------| | Sampled (10%) | Full session from start | | Not sampled + error | 60 seconds before error + everything after | | Not sampled + no error | Nothing sent (zero network cost) |
Privacy & Data Protection
Session Replay is designed with privacy as the default. No configuration is required for most applications - sensitive data is automatically protected.
Privacy Modes
| Mode | Text | Inputs | Use Case |
|------|------|--------|----------|
| "strict" (default) | Masked | Masked | Most applications |
| "balanced" | Visible | Masked | Marketing sites, blogs |
| "permissive" | Visible | Visible | Internal tools only |
replay: {
enabled: true,
privacyMode: "strict", // Default - masks everything
}What Each Mode Does
Strict Mode (Default)
All text content is replaced with ****. Form inputs show •••••. You see the page structure and user interactions, but no actual content.
┌─────────────────────────────┐
│ **** ******** │ ← Masked heading
│ │
│ ******** **** ** ******* │ ← Masked paragraph
│ ******* ** *** **** │
│ │
│ Email: [•••••••••••••] │ ← Masked input
│ Password: [••••••••] │
│ │
│ [**********] │ ← Masked button
└─────────────────────────────┘Balanced Mode
Text is visible, but all form inputs are masked. Good for content-heavy sites.
┌─────────────────────────────┐
│ Welcome Back │ ← Visible heading
│ │
│ Please sign in to continue │ ← Visible text
│ │
│ Email: [•••••••••••••] │ ← Masked input
│ Password: [••••••••] │
│ │
│ [Sign In] │ ← Visible button
└─────────────────────────────┘Permissive Mode
Everything is recorded as-is. Only use for internal tools or with explicit user consent.
Fine-Grained Control with HTML Attributes
Regardless of privacy mode, you can control specific elements:
Block Elements Completely
Use data-sw-block to completely hide an element. It will appear as a gray placeholder in replays.
<!-- Credit card form - never record -->
<div data-sw-block>
<input type="text" placeholder="Card number" />
<input type="text" placeholder="CVV" />
</div>
<!-- Sensitive user data -->
<div data-sw-block class="user-profile">
<p>SSN: 123-45-6789</p>
</div>Mask Specific Elements
Use data-sw-mask to mask text while preserving structure (useful in balanced/permissive modes).
<!-- Mask email in a visible section -->
<p>Contact: <span data-sw-mask>[email protected]</span></p>Unmask Elements in Strict Mode
Use data-sw-unmask to show content even when using strict mode.
<!-- Show navigation labels in strict mode -->
<nav data-sw-unmask>
<a href="/">Home</a>
<a href="/products">Products</a>
<a href="/about">About</a>
</nav>
<!-- Show static content -->
<footer data-sw-unmask>
<p>© 2024 My Company</p>
</footer>rrweb Classes (Alternative)
The standard rrweb classes also work:
<div class="rr-block">Hidden content</div>
<div class="rr-mask">Masked content</div>Best Practices
Start with strict mode - It's the safest default. Only relax if needed.
Block sensitive sections entirely - For areas with PII (profile pages, account settings), use
data-sw-block.Review before production - Test your app with replay enabled and verify no sensitive data appears.
Consider user consent - For GDPR compliance, inform users that sessions may be recorded.
Use unmask sparingly - Only unmask truly static, non-sensitive content like navigation labels.
What's Automatically Protected
Even without configuration:
- All
<input>values are masked (except in permissive mode) - Password fields are always masked
- Credit card patterns are detected and masked
<script>content is never recorded<head>metadata is stripped
Sensitive Data Checklist
Before enabling replay, ensure these are blocked or masked:
- [ ] User profile information (name, email, phone)
- [ ] Payment forms and credit card inputs
- [ ] Social security numbers, government IDs
- [ ] Medical or health information
- [ ] Authentication tokens displayed on screen
- [ ] Private messages or chat content
- [ ] Financial account numbers
- [ ] Any PII in confirmation pages
Distributed Tracing
The SDK automatically injects traceparent headers into fetch/XHR requests, connecting frontend sessions to backend traces.
const spanwise = createSpanwiseBrowser({
apiKey: "sw_...",
tracing: {
enabled: true,
propagateToOrigins: [
"https://api.myapp.com",
/\.myapp\.com$/,
],
},
})Your backend will receive headers like:
traceparent: 00-{traceId}-{spanId}-01Browser Support
- Chrome 64+
- Firefox 67+
- Safari 12+
- Edge 79+
Session Replay requires:
- MutationObserver
- WeakMap
- requestAnimationFrame
Bundle Size
The base SDK is ~8KB gzipped. Session Replay adds ~40KB gzipped (rrweb).
Replay is dynamically imported only when enabled, so it won't affect your bundle if not used.
TypeScript
Full TypeScript support with exported types:
import {
createSpanwiseBrowser,
type BrowserConfig,
type SpanwiseBrowser,
type ReplayConfig,
type PrivacyMode,
type TracingConfig,
} from "@spanwise/rum"Troubleshooting
Replay not recording
- Check that
replay.enabledistrue - Verify you're within the sample rate (try
sampleRate: 1.0for testing) - Check browser console for errors
High data volume
- Reduce
sampleRate(e.g.,0.05for 5%) - Use
"strict"privacy mode (smaller payloads due to masked content)
Missing trace correlation
- Ensure
tracing.propagateToOriginsincludes your API domain - Check that your backend parses the
traceparentheader
License
MIT
