@multiplayer-app/session-recorder-browser
Version:
Multiplayer Fullstack Session Recorder for Browser
561 lines (469 loc) • 19.3 kB
Markdown
# Multiplayer Session Recorder
The Multiplayer **Session Recorder** is a powerful tool that offers deep session replays with insights spanning frontend screens, platform traces, metrics, and logs. It helps your team pinpoint and resolve bugs faster by providing a complete picture of your backend system architecture. No more wasted hours combing through APM data; the Multiplayer Session Recorder does it all in one place.
## Key Features
- **Reduced Inefficiencies**: Effortlessly capture the exact steps to reproduce an issue along with backend data in one click. No more hunting through scattered documentation, APM data, logs, or traces.
- **Faster Cross-Team Alignment**: Engineers can share session links containing all relevant information, eliminating the need for long tickets or clarifying issues through back-and-forth communication.
- **Uninterrupted Deep Work**: All system information—from architecture diagrams to API designs—is consolidated in one place. Minimize context switching and stay focused on what matters.
## Getting Started
### Installation
You can install the Multiplayer Session Recorder using npm or yarn:
```bash
npm install @multiplayer-app/session-recorder-browser
# or
yarn add @multiplayer-app/session-recorder-browser
```
### Basic Setup
To initialize the Multiplayer Session Recorder in your application, follow the steps below.
#### Import the Session Recorder
```javascript
import SessionRecorder from '@multiplayer-app/session-recorder-browser'
```
#### Initialization
Use the following code to initialize the session recorder with your application details:
```javascript
SessionRecorder.init({
version: '{YOUR_APPLICATION_VERSION}',
application: '{YOUR_APPLICATION_NAME}',
environment: '{YOUR_APPLICATION_ENVIRONMENT}',
apiKey: '{YOUR_API_KEY}'
})
```
Replace the placeholders with your application’s version, name, environment, and API key (OpenTelemetry Frontend Token).
#### Add User attributes
To track user-specific attributes in session replays, add the following:
```javascript
SessionRecorder.setSessionAttributes({
userId: '{userId}',
userName: '{userName}'
})
```
Replace the placeholders with the actual user information (e.g., user ID and username).
## Dependencies
This library relies on the following packages:
- **[rrweb](https://github.com/rrweb-io/rrweb)**: Provides the frontend session replay functionality, recording the user’s interactions with the app.
- **[OpenTelemetry](https://opentelemetry.io/)**: Used to capture backend traces, metrics, and logs that integrate seamlessly with the session replays for comprehensive debugging.
## Configuration Options
The Session Recorder supports various configuration options with sensible defaults:
### Default Values
- `showWidget`: `true` - Show the recording widget by default
- `recordCanvas`: `false` - Disable canvas recording by default
- `docTraceRatio`: `0.15` - 15% of traces for auto-documentation
- `sampleTraceRatio`: `0.15` - 15% sampling ratio
- `schemifyDocSpanPayload`: `true` - Enable payload schematization
- `maxCapturingHttpPayloadSize`: `100000` - 100KB max payload size
- `usePostMessageFallback`: `false` - Disable post message fallback
- `widgetButtonPlacement`: `'bottom-right'` - Default widget position
- `masking.maskAllInputs`: `true` - Mask all inputs by default
- `masking.isMaskingEnabled`: `true` - Enable masking for debug span payload by default
- `captureBody`: `true` - Capture body in traces by default
- `captureHeaders`: `true` - Capture headers in traces by default
## Example Usage
```javascript
import SessionRecorder from '@multiplayer-app/debugger-browser'
SessionRecorder.init({
version: '1.0.0',
application: 'my-app',
environment: 'production',
apiKey: 'your-api-key',
showWidget: true,
recordCanvas: true,
ignoreUrls: [
/https:\/\/domain\.to\.ignore\/.*/, // can be regex or string
/https:\/\/another\.domain\.to\.ignore\/.*/
],
// NOTE: if frontend domain doesn't match to backend one, set backend domain to `propagateTraceHeaderCorsUrls` parameter
propagateTraceHeaderCorsUrls: [
new RegExp('https://your.backend.api.domain', 'i'), // can be regex or string
new RegExp('https://another.backend.api.domain', 'i')
],
docTraceRatio: 0.15, // 15% of traces will be sent for auto-documentation
sampleTraceRatio: 0.15, // 15% sampling ratio
schemifyDocSpanPayload: true,
maxCapturingHttpPayloadSize: 100000,
usePostMessageFallback: false, // Enable post message fallback if needed
exporterApiBaseUrl: 'https://api.multiplayer.app', // Custom API base URL (optional)
captureBody: true, // Capture body in traces
captureHeaders: true, // Capture headers in traces
// Configure masking for sensitive data in session recordings
masking: {
maskAllInputs: true, // Masks all input fields by default
maskInputOptions: {
password: true, // Always mask password fields
email: false, // Don't mask email fields by default
tel: false, // Don't mask telephone fields by default
number: false, // Don't mask number fields by default
url: false, // Don't mask URL fields by default
search: false, // Don't mask search fields by default
textarea: false // Don't mask textarea elements by default
},
// Class-based masking
maskTextClass: /sensitive|private/, // Mask text in elements with these classes
// CSS selector for text masking
maskTextSelector: '.sensitive-data', // Mask text in elements matching this selector
// Custom masking functions
maskInput: (text, element) => {
if (element.classList.contains('credit-card')) {
return '****-****-****-' + text.slice(-4)
}
return '***MASKED***'
},
maskText: (text, element) => {
if (element.dataset.type === 'email') {
const [local, domain] = text.split('@')
return local.charAt(0) + '***@' + domain
}
return '***MASKED***'
},
maskConsoleEvent: (payload) => {
// Custom console event masking
if (payload && payload.payload && payload.payload.args) {
// Mask sensitive console arguments
payload.payload.args = payload.payload.args.map((arg) =>
typeof arg === 'string' && arg.includes('password') ? '***MASKED***' : arg
)
}
return payload
},
isMaskingEnabled: true, // Enable masking for debug span payload in traces
maskBody: (payload, span) => {
// Custom trace payload masking
if (payload && typeof payload === 'object') {
const maskedPayload = { ...payload }
// Mask sensitive trace data
if (maskedPayload.requestHeaders) {
maskedPayload.requestHeaders = '***MASKED***'
}
if (maskedPayload.responseBody) {
maskedPayload.responseBody = '***MASKED***'
}
return maskedPayload
}
return payload
},
maskHeaders: (headers, span) => {
// Custom headers masking
if (headers && typeof headers === 'object') {
const maskedHeaders = { ...headers }
// Mask sensitive headers
if (maskedHeaders.authorization) {
maskedHeaders.authorization = '***MASKED***'
}
if (maskedHeaders.cookie) {
maskedHeaders.cookie = '***MASKED***'
}
return maskedHeaders
}
return headers
},
// List of body fields to mask in traces
maskBodyFieldsList: ['password', 'token', 'secret'],
// List of headers to mask in traces
maskHeadersList: ['authorization', 'cookie', 'x-api-key'],
// List of headers to include in traces (if specified, only these headers will be captured)
headersToInclude: ['content-type', 'user-agent'],
// List of headers to exclude from traces
headersToExclude: ['authorization', 'cookie']
}
})
SessionRecorder.setSessionAttributes({
userId: '12345',
userName: 'John Doe'
})
```
## API Methods
The Session Recorder provides several methods for controlling session recording:
### Session Control
- `SessionRecorder.start(type?, session?)` - Start a new session with optional existing session
- `type`: Optional `SessionType.PLAIN` or `SessionType.CONTINUOUS`, default: `SessionType.PLAIN`
- `session`: Optional existing session object
- `SessionRecorder.stop(comment?)` - Stop the current session with optional comment
- `SessionRecorder.pause()` - Pause the current session
- `SessionRecorder.resume()` - Resume the current session
- `SessionRecorder.cancel()` - Cancel the current session
- `SessionRecorder.save()` - Save the continuous recording session
### Configuration
- `SessionRecorder.setSessionAttributes(attributes)` - Set session metadata
- `SessionRecorder.recordingButtonClickHandler = handler` - Set custom click handler
### Properties
- `SessionRecorder.sessionId` - Get current session ID (readonly)
- `SessionRecorder.sessionType` - Get current session type (readonly)
- `SessionRecorder.sessionState` - Get current session state (readonly)
- `SessionRecorder.session` - Get current session object (readonly)
- `SessionRecorder.sessionAttributes` - Get current session attributes (readonly)
- `SessionRecorder.error` - Get/set error message
- `SessionRecorder.sessionWidgetButtonElement` - Get the widget button element (readonly)
### Session Types
- `SessionType.PLAIN` - Standard session recording
- `SessionType.CONTINUOUS` - Continuous recording session
### Session States
- `SessionState.started` - Session is currently recording
- `SessionState.paused` - Session is paused
- `SessionState.stopped` - Session is stopped
### Session Attributes
You can set various session attributes for better tracking:
```javascript
SessionRecorder.setSessionAttributes({
userId: '12345',
userName: 'John Doe',
userEmail: 'john@example.com',
accountId: 'acc_123',
accountName: 'Enterprise Account'
})
```
## Masking Configuration
The Session Recorder includes comprehensive masking options to protect sensitive data during session recordings. You can configure masking behavior through the `masking` option:
### Basic Masking Options
- `maskAllInputs`: If `true`, masks all input fields in the recording (default: `true`)
- `isMaskingEnabled`: If `true`, enables masking for debug span payload in traces (default: `true`)
### Input Type Masking
You can control masking for specific input types:
```javascript
maskInputOptions: {
password: true, // Always mask password fields (default: true)
email: false, // Don't mask email fields by default
tel: false, // Don't mask telephone fields by default
number: false, // Don't mask number fields by default
url: false, // Don't mask URL fields by default
search: false, // Don't mask search fields by default
textarea: false, // Don't mask textarea elements by default
select: false, // Don't mask select elements by default
// ...other types
}
```
### CSS Selector Masking
You can mask specific elements using CSS selectors:
```javascript
masking: {
// Mask text in elements matching this selector
maskTextSelector: '.sensitive-data, [data-private="true"], .user-profile .email',
}
```
### Class-Based Masking
You can mask text based on CSS classes using string or RegExp patterns:
```javascript
masking: {
maskTextClass: 'sensitive', // Mask text in elements with class 'sensitive'
}
```
Or with RegExp pattern:
```javascript
masking: {
maskTextClass: /private|confidential/, // Mask text in elements with classes 'private' or 'confidential'
}
```
### Custom Masking Functions
For advanced masking scenarios, you can provide custom functions:
```javascript
masking: {
// Custom function for input masking
maskInput: (text, element) => {
// Custom logic to mask input text
if (element.classList.contains('credit-card')) {
return '****-****-****-' + text.slice(-4);
}
return '***MASKED***';
},
// Custom function for text masking
maskText: (text, element) => {
// Custom logic to mask text content
if (element.dataset.type === 'email') {
const [local, domain] = text.split('@');
return local.charAt(0) + '***@' + domain;
}
return '***MASKED***';
},
// Custom function for masking body in traces
maskBody: (payload, span) => {
// Custom logic to mask sensitive data in trace payloads
if (payload && typeof payload === 'object') {
const maskedPayload = { ...payload };
// Mask sensitive fields
if (maskedPayload.headers) {
maskedPayload.headers = '***MASKED***';
}
if (maskedPayload.body) {
maskedPayload.body = '***MASKED***';
}
return maskedPayload;
}
return payload;
},
// Custom function for masking headers in traces
maskHeaders: (headers, span) => {
// Custom logic to mask sensitive headers
if (headers && typeof headers === 'object') {
const maskedHeaders = { ...headers };
// Mask sensitive headers
if (maskedHeaders.authorization) {
maskedHeaders.authorization = '***MASKED***';
}
if (maskedHeaders.cookie) {
maskedHeaders.cookie = '***MASKED***';
}
return maskedHeaders;
}
return headers;
},
}
```
### Example: Comprehensive Masking Setup
```javascript
SessionRecorder.init({
// ... other options
masking: {
maskAllInputs: true,
maskInputOptions: {
password: true,
email: true, // Mask email fields for privacy
tel: true, // Mask telephone fields for privacy
number: false, // Allow number fields
url: false, // Allow URL fields
search: false, // Allow search fields
textarea: false // Allow textarea elements
// ...other types
},
maskTextClass: /sensitive|private|confidential/, // Mask text in elements with these classes
maskTextSelector: '.user-email, .user-phone, .credit-card, [data-sensitive="true"]', // Mask text in elements matching this selector
maskInput: (text, element) => {
// Custom credit card masking
if (element.classList.contains('credit-card')) {
return '****-****-****-' + text.slice(-4)
}
return '***MASKED***'
},
maskText: (text, element) => {
// Custom email masking
if (element.dataset.type === 'email') {
const [local, domain] = text.split('@')
return local.charAt(0) + '***@' + domain
}
return '***MASKED***'
},
maskConsoleEvent: (payload) => {
// Custom console event masking
if (payload && payload.payload && payload.payload.args) {
payload.payload.args = payload.payload.args.map((arg) =>
typeof arg === 'string' && arg.includes('password') ? '***MASKED***' : arg
)
}
return payload
},
isMaskingEnabled: true, // Enable masking for debug span payload in traces
maskBody: (payload, span) => {
// Custom trace payload masking
if (payload && typeof payload === 'object') {
const maskedPayload = { ...payload }
// Mask sensitive trace data
if (maskedPayload.requestHeaders) {
maskedPayload.requestHeaders = '***MASKED***'
}
if (maskedPayload.responseBody) {
maskedPayload.responseBody = '***MASKED***'
}
return maskedPayload
}
return payload
},
maskHeaders: (headers, span) => {
// Custom headers masking
if (headers && typeof headers === 'object') {
const maskedHeaders = { ...headers }
// Mask sensitive headers
if (maskedHeaders.authorization) {
maskedHeaders.authorization = '***MASKED***'
}
if (maskedHeaders.cookie) {
maskedHeaders.cookie = '***MASKED***'
}
return maskedHeaders
},
// List of body fields to mask in traces
maskBodyFieldsList: ['password', 'token', 'secret'],
// List of headers to mask in traces
maskHeadersList: ['authorization', 'cookie', 'x-api-key'],
// List of headers to include in traces (if specified, only these headers will be captured)
headersToInclude: ['content-type', 'user-agent'],
// List of headers to exclude from traces
headersToExclude: ['authorization', 'cookie']
}
})
```
## Session Recorder for Next.js
To integrate the MySessionRecorder component into your Next.js application, follow these steps:
- Create a new file (e.g., MySessionRecorder.js or MySessionRecorder.tsx) in your root directory or a components directory.
- Import the component
In the newly created file, add the following code:
```javascript
'use client' // Mark as Client Component
import { useEffect } from 'react'
import SessionRecorder from '@multiplayer-app/session-recorder-browser'
export default function MySessionRecorder() {
useEffect(() => {
if (typeof window !== 'undefined') {
SessionRecorder.init({
version: '{YOUR_APPLICATION_VERSION}',
application: '{YOUR_APPLICATION_NAME}',
environment: '{YOUR_APPLICATION_ENVIRONMENT}',
apiKey: '{YOUR_API_KEY}',
recordCanvas: true, // Enable canvas recording
masking: {
maskAllInputs: true,
maskInputOptions: {
password: true,
email: false,
tel: false
}
}
})
SessionRecorder.setSessionAttributes({
userId: '{userId}',
userName: '{userName}'
})
}
}, [])
return null // No UI output needed
}
```
Replace the placeholders with the actual information.
Now, you can use the MySessionRecorder component in your application by adding it to your desired page or layout file:
```javascript
import MySessionRecorder from './MySessionRecorder' // Adjust the path as necessary
export default function MyApp() {
return (
<>
<MySessionRecorder />
{/* Other components */}
</>
)
}
```
## Note
If frontend domain doesn't match to backend one, set backend domain to `propagateTraceHeaderCorsUrls` parameter:
```javascript
import SessionRecorder from '@multiplayer-app/session-recorder-browser'
SessionRecorder.init({
version: '{YOUR_APPLICATION_VERSION}',
application: '{YOUR_APPLICATION_NAME}',
environment: '{YOUR_APPLICATION_ENVIRONMENT}',
apiKey: '{YOUR_API_KEY}',
propagateTraceHeaderCorsUrls: new RegExp(`https://your.backend.api.domain`, 'i')
})
```
If frontend sends api requests to two or more different domains put them to `propagateTraceHeaderCorsUrls` as array:
```javascript
import SessionRecorder from '@multiplayer-app/session-recorder-browser'
SessionRecorder.init({
version: '{YOUR_APPLICATION_VERSION}',
application: '{YOUR_APPLICATION_NAME}',
environment: '{YOUR_APPLICATION_ENVIRONMENT}',
apiKey: '{YOUR_API_KEY}',
propagateTraceHeaderCorsUrls: [
new RegExp(`https://your.backend.api.domain`, 'i'),
new RegExp(`https://another.backend.api.domain`, 'i')
]
})
```
## Documentation
For more details on how the Multiplayer Session Recorder integrates with your backend architecture and system auto-documentation, check out our [official documentation](https://www.multiplayer.app/docs/features/system-auto-documentation/).
## License
This library is distributed under the [MIT License](LICENSE).