npm package: @spanwise/rum
Description: Real User Monitoring SDK for Spanwise - track sessions, page views, clicks, and Core Web Vitals
Latest version: 0.8.0

# @spanwise/rum

Real User Monitoring SDK for browser applications. Captures page views, clicks, errors, Web Vitals, and session replays.

## Installation

```bash
npm install @spanwise/rum
# or
pnpm add @spanwise/rum
# or
yarn add @spanwise/rum
```

## Quick Start

```typescript
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

```typescript
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.

```typescript
spanwise.trackEvent("purchase", {
  productId: "123",
  amount: 99.99,
})
```

### `spanwise.trackPageView(url?)`

Manually track a page view. Called automatically on navigation.

```typescript
spanwise.trackPageView("/checkout")
```

### `spanwise.trackError(error, options?)`

Manually track an error.

```typescript
try {
  await riskyOperation()
} catch (error) {
  spanwise.trackError(error, {
    requestId: "req_123",
    traceId: "trace_abc",
  })
}
```

### `spanwise.setUser(id, traits?)`

Identify the current user.

```typescript
spanwise.setUser("user_123", {
  email: "user@example.com",
  plan: "pro",
})
```

### `spanwise.getSessionId()`

Get the current session ID.

```typescript
const sessionId = spanwise.getSessionId()
```

### `spanwise.forceReplayUpload()`

Force replay upload for specific users (bypasses sampling).

```typescript
// 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

```typescript
const spanwise = createSpanwiseBrowser({
  apiKey: "sw_...",
  replay: {
    enabled: true,
  },
})
```

### How It Works: Rolling Buffer

Session Replay uses a **rolling buffer architecture** (similar to Sentry):

1. **Always recording** - When enabled, the SDK always records to a 60-second memory buffer
2. **Sampled sessions** (default 10%) - Upload immediately to server
3. **Non-sampled sessions** (90%) - Keep buffer in memory, zero network cost
4. **On error** - Flush the 60-second buffer and start uploading

This means you **always capture what happened before an error**, not just after.

### Sampling

```typescript
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 |

```typescript
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.

```html
<!-- 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).

```html
<!-- Mask email in a visible section -->
<p>Contact: <span data-sw-mask>user@example.com</span></p>
```

#### Unmask Elements in Strict Mode

Use `data-sw-unmask` to show content even when using strict mode.

```html
<!-- 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:

```html
<div class="rr-block">Hidden content</div>
<div class="rr-mask">Masked content</div>
```

### Best Practices

1. **Start with strict mode** - It's the safest default. Only relax if needed.

2. **Block sensitive sections entirely** - For areas with PII (profile pages, account settings), use `data-sw-block`.

3. **Review before production** - Test your app with replay enabled and verify no sensitive data appears.

4. **Consider user consent** - For GDPR compliance, inform users that sessions may be recorded.

5. **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.

```typescript
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}-01
```

---

## Browser 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:

```typescript
import {
  createSpanwiseBrowser,
  type BrowserConfig,
  type SpanwiseBrowser,
  type ReplayConfig,
  type PrivacyMode,
  type TracingConfig,
} from "@spanwise/rum"
```

---

## Troubleshooting

### Replay not recording

1. Check that `replay.enabled` is `true`
2. Verify you're within the sample rate (try `sampleRate: 1.0` for testing)
3. Check browser console for errors

### High data volume

1. Reduce `sampleRate` (e.g., `0.05` for 5%)
2. Use `"strict"` privacy mode (smaller payloads due to masked content)

### Missing trace correlation

1. Ensure `tracing.propagateToOrigins` includes your API domain
2. Check that your backend parses the `traceparent` header

---

## License

MIT