# @supastory/capture-sdk

Session replay SDK for capturing user interactions. Records DOM changes, clicks, scrolls, network requests, and more for playback in the SupaStory dashboard.

## Installation

```bash
npm install @supastory/capture-sdk
# or
yarn add @supastory/capture-sdk
# or
pnpm add @supastory/capture-sdk
```

## Quick Start

```javascript
import SupaStory from '@supastory/capture-sdk';

SupaStory.init({
  projectKey: 'pk_live_your_project_key',
});
```

## Usage

### Basic Initialization

```javascript
import SupaStory from '@supastory/capture-sdk';

SupaStory.init({
  projectKey: 'pk_live_abc123',
});
```

### With Callbacks

```javascript
SupaStory.init({
  projectKey: 'pk_live_abc123',
  onReady: (sessionId) => {
    console.log('Recording started:', sessionId);
    // Link session to your analytics
    analytics.track('session_started', { sessionId });
  },
  onError: (error) => {
    // Send to your error tracking service
    errorTracker.capture(error);
  },
});
```

### With Consent Flow

```javascript
// Initialize without auto-starting
SupaStory.init({
  projectKey: 'pk_live_abc123',
  autoStart: false,
  onReady: (sessionId) => {
    console.log('Recording started:', sessionId);
  },
});

// Later, after user consent
function handleConsentAccepted() {
  SupaStory.start();
}

function handleConsentDeclined() {
  // Session was initialized but never started recording
  SupaStory.stop();
}
```

### Script Tag Usage

```html
<script src="https://app.supastory.com/api/sdk.js"></script>
<script>
  SupaStory.init({
    projectKey: 'pk_live_abc123',
  });
</script>
```

## Configuration Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `projectKey` | `string` | **required** | Your project key from the SupaStory dashboard |
| `endpoint` | `string` | `'https://api.supastory.com/ingest'` | API endpoint for sending events |
| `sampleRate` | `number` | `1.0` | Session sampling rate (0.0 - 1.0) |
| `maskInputs` | `boolean` | `true` | Mask password and sensitive inputs |
| `maskAllInputs` | `boolean` | `false` | Mask all input fields |
| `excludeSelectors` | `string[]` | `[]` | CSS selectors to exclude from capture |
| `batchSize` | `number` | `50` | Events per batch |
| `batchIntervalMs` | `number` | `5000` | Batch send interval |
| `maxSessionDurationMs` | `number` | `1800000` | Max session duration (30 min) |
| `captureNetworkRequests` | `boolean` | `true` | Capture XHR/Fetch requests |
| `captureNetworkPayloads` | `boolean` | `true` | Include request/response bodies |
| `startDelayMs` | `number` | `5000` | Delay before starting capture |
| `autoStart` | `boolean` | `true` | Auto-start recording on init |
| `logLevel` | `LogLevel` | `'none'` | Logging level |
| `onReady` | `function` | - | Called when recording starts |
| `onError` | `function` | - | Called on SDK errors |
| `onSampledOut` | `function` | - | Called when session is not sampled |

### Log Levels

```javascript
type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'verbose';
```

- `'none'` - No logging (production default)
- `'error'` - Only critical errors
- `'warn'` - Errors and warnings
- `'info'` - General information
- `'verbose'` - Detailed debugging

## API Reference

### `init(config)`

Initialize the SDK. Call once when your app loads.

```javascript
SupaStory.init({
  projectKey: 'pk_live_abc123',
  sampleRate: 0.1, // Capture 10% of sessions
  logLevel: 'info',
});
```

### `start()`

Manually start recording when `autoStart: false`. Use for consent flows.

```javascript
// Initialize without auto-start for consent flow
SupaStory.init({
  projectKey: 'pk_live_abc123',
  autoStart: false,
});

// Later, after user consents
function handleConsentAccepted() {
  SupaStory.start();
}
```

### `stop()`

Stop recording and clean up. Flushes remaining events.

```javascript
// Stop when user logs out
function handleLogout() {
  SupaStory.stop();
}
```

### `pause()` / `resume()`

Temporarily pause and resume recording.

```javascript
// Pause during sensitive operations
SupaStory.pause();
await showSensitiveModal();
SupaStory.resume();
```

### `getSessionId()`

Get the current session ID for linking to support tools.

```javascript
const sessionId = SupaStory.getSessionId();
if (sessionId) {
  supportTicket.replayUrl = `https://app.supastory.com/replay/${sessionId}`;
}
```

### `isRecording()`

Check if currently recording.

```javascript
if (SupaStory.isRecording()) {
  console.log('Session replay is active');
}
```

### `getStatus()`

Get detailed SDK status for debugging.

```javascript
const status = SupaStory.getStatus();
console.log({
  initialized: status.initialized,
  recording: status.recording,
  eventCount: status.eventCount,
  sampledOut: status.sampledOut,
});
```

Returns:
```typescript
interface SDKStatus {
  initialized: boolean;
  recording: boolean;
  paused: boolean;
  sessionId: string | null;
  visitorId: string | null;
  queueSize: number;
  eventCount: number;
  sampledOut: boolean;
}
```

### `identify(userId, traits?)`

Link sessions to your users.

```javascript
// After user login
SupaStory.identify('user_123', {
  email: 'user@example.com',
  plan: 'premium',
  company: 'Acme Inc',
});
```

### `setLogLevel(level)`

Change log level at runtime.

```javascript
// Enable verbose logging for debugging
SupaStory.setLogLevel('verbose');

// Disable logging
SupaStory.setLogLevel('none');
```

## Error Handling

The SDK never throws exceptions. Errors are passed to the `onError` callback.

```javascript
SupaStory.init({
  projectKey: 'pk_live_abc123',
  onError: (error) => {
    // Error structure
    // {
    //   code: 'NETWORK_ERROR',
    //   message: 'Failed to send events',
    //   originalError?: Error,
    //   context?: { ... }
    // }

    switch (error.code) {
      case 'NETWORK_ERROR':
        // Network issues - events will be retried
        break;
      case 'STORAGE_ERROR':
        // localStorage/sessionStorage unavailable
        break;
      case 'NOT_INITIALIZED':
        // Called method before init()
        break;
    }
  },
});
```

### Error Codes

| Code | Description |
|------|-------------|
| `ALREADY_INITIALIZED` | `init()` called twice |
| `NOT_INITIALIZED` | Method called before `init()` |
| `MISSING_PROJECT_KEY` | No projectKey provided |
| `INVALID_CONFIG` | Invalid configuration value |
| `NETWORK_ERROR` | Failed to send events |
| `STORAGE_ERROR` | Storage API unavailable |
| `SESSION_NOT_SAMPLED` | Session excluded by sample rate |

## Privacy & Security

### Input Masking

By default, password fields and inputs with `type="password"` are masked.

```javascript
SupaStory.init({
  projectKey: 'pk_live_abc123',
  maskInputs: true,      // Mask sensitive inputs (default)
  maskAllInputs: false,  // Don't mask all inputs
});
```

### Excluding Elements

Exclude sensitive elements from capture:

```javascript
SupaStory.init({
  projectKey: 'pk_live_abc123',
  excludeSelectors: [
    '.sensitive-data',
    '[data-private]',
    '#credit-card-form',
  ],
});
```

Or use data attributes to exclude elements from capture:

```html
<div data-no-record>
  This content won't be captured
</div>

<!-- Alternative attribute -->
<div data-private>
  This content won't be captured either
</div>
```

You can also use CSS classes:

```html
<div class="no-record">Excluded</div>
<div class="private">Also excluded</div>
```

### Competitor Attribute Compatibility

If you're migrating from another session replay tool, SupaStory automatically recognizes their attributes — no code changes needed:

| Provider | Exclude Element | Mask Input |
|----------|----------------|------------|
| Sentry | `data-sentry-block` | `data-sentry-mask`, `data-sentry-block` |
| FullStory | `data-fs-hidden`, `data-fs-block` | `data-fs-mask`, `data-fs-hidden`, `data-fs-block` |
| OpenReplay | `data-openreplay-hidden` | `data-openreplay-mask`, `data-openreplay-hidden` |
| LogRocket | — | `data-logrocket-mask` |
| PostHog | `ph-no-capture` | `ph-no-capture` |

## Testing

The SDK exports test utilities for mocking in your tests:

```javascript
import { createMockSDK, createMockConfig } from '@supastory/capture-sdk/testing';

describe('MyComponent', () => {
  let mockSDK;

  beforeEach(() => {
    mockSDK = createMockSDK();
  });

  afterEach(() => {
    mockSDK._reset();
  });

  it('initializes SupaStory on mount', () => {
    mockSDK.init(createMockConfig());

    expect(mockSDK.isRecording()).toBe(true);
    expect(mockSDK._getCallHistory().init).toHaveLength(1);
  });

  it('identifies user after login', () => {
    mockSDK.init(createMockConfig());
    mockSDK.identify('user_123', { plan: 'premium' });

    const history = mockSDK._getCallHistory();
    expect(history.identify[0]).toEqual({
      userId: 'user_123',
      traits: { plan: 'premium' },
    });
  });
});
```

### Test Utilities

- `createMockSDK()` - Creates a mock SDK instance
- `createMockConfig(overrides?)` - Creates valid test config
- `createErrorCapture()` - Captures errors for assertions
- `createReadyCapture()` - Captures onReady callbacks

## Framework Integration

### React

```jsx
import { useEffect } from 'react';
import SupaStory from '@supastory/capture-sdk';

function App() {
  useEffect(() => {
    SupaStory.init({
      projectKey: process.env.REACT_APP_SUPASTORY_KEY,
      onReady: (sessionId) => {
        console.log('Session:', sessionId);
      },
    });

    return () => SupaStory.stop();
  }, []);

  return <YourApp />;
}
```

### Next.js

```jsx
// app/providers.tsx
'use client';

import { useEffect } from 'react';
import SupaStory from '@supastory/capture-sdk';

export function Providers({ children }) {
  useEffect(() => {
    SupaStory.init({
      projectKey: process.env.NEXT_PUBLIC_SUPASTORY_KEY,
    });

    return () => SupaStory.stop();
  }, []);

  return children;
}
```

### Vue

```javascript
// main.js
import { createApp } from 'vue';
import SupaStory from '@supastory/capture-sdk';
import App from './App.vue';

SupaStory.init({
  projectKey: import.meta.env.VITE_SUPASTORY_KEY,
});

createApp(App).mount('#app');
```

## Troubleshooting

### SDK not capturing events

1. Check if initialized: `SupaStory.getStatus().initialized`
2. Check if recording: `SupaStory.isRecording()`
3. Check if sampled out: `SupaStory.getStatus().sampledOut`
4. Enable verbose logging: `SupaStory.setLogLevel('verbose')`

### Events not appearing in dashboard

1. Check network tab for failed requests to `/ingest`
2. Verify your `projectKey` is correct
3. Check `onError` callback for errors

### Performance concerns

1. Reduce `batchSize` for more frequent, smaller batches
2. Increase `batchIntervalMs` for less frequent sends
3. Use `sampleRate` to capture fewer sessions
4. Use `excludeSelectors` for high-frequency DOM updates

## TypeScript

Full TypeScript support with exported types:

```typescript
import SupaStory, {
  type SDKConfig,
  type SDKStatus,
  type SDKError,
  type LogLevel,
} from '@supastory/capture-sdk';

const config: SDKConfig = {
  projectKey: 'pk_live_abc123',
  onError: (error: SDKError) => {
    console.error(error.code, error.message);
  },
};

SupaStory.init(config);

const status: SDKStatus = SupaStory.getStatus();
```

## License

See [LICENSE](./LICENSE) for details.
