@multiplayer-app/session-recorder-browser 1.2.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -1
- package/README.md +143 -470
- package/dist/browser/index.js +6 -14
- package/dist/browser/index.js.map +1 -1
- package/dist/exporters/index.js +1 -1
- package/dist/exporters/index.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/otel/helpers.d.ts.map +1 -1
- package/dist/types/sessionRecorder.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,101 +1,104 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<a href="https://github.com/multiplayer-app/multiplayer-session-recorder-javascript">
|
|
5
|
+
<img src="https://img.shields.io/github/stars/multiplayer-app/multiplayer-session-recorder-javascript?style=social&label=Star&maxAge=2592000" alt="GitHub stars">
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://github.com/multiplayer-app/multiplayer-session-recorder-javascript/blob/main/LICENSE">
|
|
8
|
+
<img src="https://img.shields.io/github/license/multiplayer-app/multiplayer-session-recorder-javascript" alt="License">
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://multiplayer.app">
|
|
11
|
+
<img src="https://img.shields.io/badge/Visit-multiplayer.app-blue" alt="Visit Multiplayer">
|
|
12
|
+
</a>
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
<div>
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="https://x.com/trymultiplayer">
|
|
18
|
+
<img src="https://img.shields.io/badge/Follow%20on%20X-000000?style=for-the-badge&logo=x&logoColor=white" alt="Follow on X" />
|
|
19
|
+
</a>
|
|
20
|
+
<a href="https://www.linkedin.com/company/multiplayer-app/">
|
|
21
|
+
<img src="https://img.shields.io/badge/Follow%20on%20LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white" alt="Follow on LinkedIn" />
|
|
22
|
+
</a>
|
|
23
|
+
<a href="https://discord.com/invite/q9K3mDzfrx">
|
|
24
|
+
<img src="https://img.shields.io/badge/Join%20our%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Join our Discord" />
|
|
25
|
+
</a>
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
# Multiplayer Full Stack Session Recorder
|
|
30
|
+
|
|
31
|
+
The Multiplayer Full Stack 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 Full Stack Session Recorder does it all in one place.
|
|
32
|
+
|
|
33
|
+
## What you get
|
|
34
|
+
|
|
35
|
+
- Full stack replays: browser screen recording correlated with OTLP traces and logs
|
|
36
|
+
- One‑click shareable sessions: Engineers can share session links containing all relevant information, eliminating the need for long tickets or clarifying issues through back-and-forth communication.
|
|
37
|
+
- Privacy by default: input/text masking and trace payload/header masking
|
|
38
|
+
- Flexible: works with any web app; Node SDK for backend correlation
|
|
39
|
+
- Lightweight widget: start/pause/stop/save controls for your users or QA
|
|
12
40
|
|
|
13
41
|
### Installation
|
|
14
42
|
|
|
15
|
-
You can install the Multiplayer Session Recorder using npm or yarn:
|
|
16
|
-
|
|
17
43
|
```bash
|
|
18
|
-
npm
|
|
44
|
+
npm i @multiplayer-app/session-recorder-browser @opentelemetry/api
|
|
19
45
|
# or
|
|
20
|
-
yarn add @multiplayer-app/session-recorder-browser
|
|
46
|
+
yarn add @multiplayer-app/session-recorder-browser @opentelemetry/api
|
|
21
47
|
```
|
|
22
48
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
To initialize the Multiplayer Session Recorder in your application, follow the steps below.
|
|
26
|
-
|
|
27
|
-
#### Import the Session Recorder
|
|
49
|
+
## Set up web client:
|
|
28
50
|
|
|
51
|
+
### Quick start
|
|
29
52
|
```javascript
|
|
30
53
|
import SessionRecorder from '@multiplayer-app/session-recorder-browser'
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
#### Initialization
|
|
34
54
|
|
|
35
|
-
Use the following code to initialize the session recorder with your application details:
|
|
36
|
-
|
|
37
|
-
```javascript
|
|
38
55
|
SessionRecorder.init({
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
environment: '
|
|
42
|
-
apiKey: '
|
|
56
|
+
application: 'my-web-app',
|
|
57
|
+
version: '1.0.0',
|
|
58
|
+
environment: 'production',
|
|
59
|
+
apiKey: '<YOUR_FRONTEND_OTEL_TOKEN>',
|
|
60
|
+
// IMPORTANT: in order to propagate OTLP headers to a backend
|
|
61
|
+
// domain(s) with a different origin, add backend domain(s) below.
|
|
62
|
+
// e.g. if you serve your website from www.example.com
|
|
63
|
+
// and your backend domain is at api.example.com set value as shown below:
|
|
64
|
+
// format: string|RegExp|Array
|
|
65
|
+
// propagateTraceHeaderCorsUrls: [new RegExp('https://api.example.com', 'i')],
|
|
43
66
|
})
|
|
44
|
-
```
|
|
45
67
|
|
|
46
|
-
Replace the placeholders with your application’s version, name, environment, and API key (OpenTelemetry Frontend Token).
|
|
47
68
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
To track user-specific attributes in session replays, add the following:
|
|
51
|
-
|
|
52
|
-
```javascript
|
|
69
|
+
// add any key value pairs which should be associated with a session
|
|
53
70
|
SessionRecorder.setSessionAttributes({
|
|
54
|
-
userId: '
|
|
55
|
-
userName: '
|
|
71
|
+
userId: '12345',
|
|
72
|
+
userName: 'Jane Doe',
|
|
56
73
|
})
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Replace the placeholders with the actual user information (e.g., user ID and username).
|
|
60
|
-
|
|
61
|
-
## Dependencies
|
|
62
|
-
|
|
63
|
-
This library relies on the following packages:
|
|
64
74
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
- `showWidget`: `true` - Show the recording widget by default
|
|
75
|
-
- `recordCanvas`: `false` - Disable canvas recording by default
|
|
76
|
-
- `docTraceRatio`: `0.15` - 15% of traces for auto-documentation
|
|
77
|
-
- `sampleTraceRatio`: `0.15` - 15% sampling ratio
|
|
78
|
-
- `schemifyDocSpanPayload`: `true` - Enable payload schematization
|
|
79
|
-
- `maxCapturingHttpPayloadSize`: `100000` - 100KB max payload size
|
|
80
|
-
- `usePostMessageFallback`: `false` - Disable post message fallback
|
|
81
|
-
- `widgetButtonPlacement`: `'bottom-right'` - Default widget position
|
|
82
|
-
- `masking.maskAllInputs`: `true` - Mask all inputs by default
|
|
83
|
-
- `masking.isMaskingEnabled`: `true` - Enable masking for debug span payload by default
|
|
84
|
-
- `captureBody`: `true` - Capture body in traces by default
|
|
85
|
-
- `captureHeaders`: `true` - Capture headers in traces by default
|
|
75
|
+
// optionally control via API (widget is enabled by default)
|
|
76
|
+
// if you're not using widget (see: `showWidget: true/false`)
|
|
77
|
+
// then you can programatically control the session recorder
|
|
78
|
+
// by using the methods below
|
|
79
|
+
SessionRecorder.start()
|
|
80
|
+
SessionRecorder.pause()
|
|
81
|
+
SessionRecorder.resume()
|
|
82
|
+
SessionRecorder.stop('Finished session') // optional: pass reason for stopping the session
|
|
83
|
+
```
|
|
86
84
|
|
|
87
|
-
|
|
85
|
+
### Advanced config
|
|
88
86
|
|
|
89
87
|
```javascript
|
|
90
88
|
import SessionRecorder from '@multiplayer-app/debugger-browser'
|
|
91
89
|
|
|
92
90
|
SessionRecorder.init({
|
|
93
|
-
version: '1.0.0',
|
|
94
|
-
application: 'my-app',
|
|
91
|
+
version: '1.0.0', // optional: version of your application
|
|
92
|
+
application: 'my-app', // name of your application
|
|
95
93
|
environment: 'production',
|
|
96
|
-
apiKey: 'your-api-key',
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
apiKey: 'your-api-key', // replace with your Multiplayer OTLP key
|
|
95
|
+
|
|
96
|
+
apiBaseUrl: 'https://api.multiplayer.app', // override API base URL if needed
|
|
97
|
+
exporterEndpoint: 'https://otlp.multiplayer.app', // override OTLP collector URL if needed
|
|
98
|
+
|
|
99
|
+
showWidget: true, // show in‑app recording widget (default: true)
|
|
100
|
+
recordCanvas: true, // record canvas elements (default: false)
|
|
101
|
+
// Add domains to not capture OTLP data in the session recording
|
|
99
102
|
ignoreUrls: [
|
|
100
103
|
/https:\/\/domain\.to\.ignore\/.*/, // can be regex or string
|
|
101
104
|
/https:\/\/another\.domain\.to\.ignore\/.*/
|
|
@@ -105,31 +108,48 @@ SessionRecorder.init({
|
|
|
105
108
|
new RegExp('https://your.backend.api.domain', 'i'), // can be regex or string
|
|
106
109
|
new RegExp('https://another.backend.api.domain', 'i')
|
|
107
110
|
],
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
|
|
112
|
+
// sample trace ratio used when session recording is not active.
|
|
113
|
+
// configures what percentage (0.00-1.00) of OTLP data
|
|
114
|
+
// should be sent through `exporters`
|
|
115
|
+
sampleTraceRatio: 0,
|
|
116
|
+
|
|
117
|
+
// optional: exporters allow you to send
|
|
118
|
+
// OTLP data to observability platforms
|
|
119
|
+
exporters: [
|
|
120
|
+
// example:
|
|
121
|
+
// import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
|
|
122
|
+
// new OTLPTraceExporter({
|
|
123
|
+
// url: '<opentelemetry-collector-url>',
|
|
124
|
+
// })
|
|
125
|
+
],
|
|
126
|
+
|
|
127
|
+
captureBody: true, // capture request/response content
|
|
128
|
+
captureHeaders: true, // capture request/response header content
|
|
129
|
+
|
|
130
|
+
// set the maximum request/response content size (in bytes) that will be captured
|
|
131
|
+
// any request/response content greater than size will be not included in session recordings
|
|
111
132
|
maxCapturingHttpPayloadSize: 100000,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
captureBody: true, // Capture body in traces
|
|
115
|
-
captureHeaders: true, // Capture headers in traces
|
|
116
|
-
// Configure masking for sensitive data in session recordings
|
|
133
|
+
|
|
134
|
+
// configure masking for sensitive data in session recordings
|
|
117
135
|
masking: {
|
|
118
|
-
maskAllInputs:
|
|
136
|
+
maskAllInputs: false, // masks all input fields
|
|
119
137
|
maskInputOptions: {
|
|
120
|
-
password: true, //
|
|
121
|
-
email: false, //
|
|
122
|
-
tel: false, //
|
|
123
|
-
number: false, //
|
|
124
|
-
url: false, //
|
|
125
|
-
search: false, //
|
|
126
|
-
textarea: false //
|
|
138
|
+
password: true, // mask password fields
|
|
139
|
+
email: false, // mask email fields
|
|
140
|
+
tel: false, // mask telephone fields
|
|
141
|
+
number: false, // mask number fields
|
|
142
|
+
url: false, // mask URL fields
|
|
143
|
+
search: false, // mask search fields
|
|
144
|
+
textarea: false // mask textarea elements
|
|
127
145
|
},
|
|
128
|
-
|
|
129
|
-
|
|
146
|
+
|
|
147
|
+
// class-based masking
|
|
148
|
+
maskTextClass: /sensitive|private/, // mask text in elements with these classes
|
|
130
149
|
// CSS selector for text masking
|
|
131
|
-
maskTextSelector: '.sensitive-data', //
|
|
132
|
-
|
|
150
|
+
maskTextSelector: '.sensitive-data', // mask text in elements matching this selector
|
|
151
|
+
|
|
152
|
+
// custom masking functions
|
|
133
153
|
maskInput: (text, element) => {
|
|
134
154
|
if (element.classList.contains('credit-card')) {
|
|
135
155
|
return '****-****-****-' + text.slice(-4)
|
|
@@ -144,417 +164,70 @@ SessionRecorder.init({
|
|
|
144
164
|
return '***MASKED***'
|
|
145
165
|
},
|
|
146
166
|
maskConsoleEvent: (payload) => {
|
|
147
|
-
// Custom console event masking
|
|
148
167
|
if (payload && payload.payload && payload.payload.args) {
|
|
149
|
-
//
|
|
168
|
+
// mask sensitive console arguments
|
|
150
169
|
payload.payload.args = payload.payload.args.map((arg) =>
|
|
151
170
|
typeof arg === 'string' && arg.includes('password') ? '***MASKED***' : arg
|
|
152
171
|
)
|
|
153
172
|
}
|
|
154
173
|
return payload
|
|
155
174
|
},
|
|
156
|
-
|
|
175
|
+
|
|
176
|
+
isContentMaskingEnabled: true, // enable content masking in session recordings
|
|
157
177
|
maskBody: (payload, span) => {
|
|
158
|
-
//
|
|
178
|
+
// note: `payload` is already a copy of the original request/response content
|
|
159
179
|
if (payload && typeof payload === 'object') {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
maskedPayload.requestHeaders = '***MASKED***'
|
|
180
|
+
// mask sensitive data
|
|
181
|
+
if (payload.requestHeaders) {
|
|
182
|
+
payload.requestHeaders = '***MASKED***'
|
|
164
183
|
}
|
|
165
|
-
if (
|
|
166
|
-
|
|
184
|
+
if (payload.responseBody) {
|
|
185
|
+
payload.responseBody = '***MASKED***'
|
|
167
186
|
}
|
|
168
|
-
return maskedPayload
|
|
169
187
|
}
|
|
170
188
|
return payload
|
|
171
189
|
},
|
|
172
190
|
maskHeaders: (headers, span) => {
|
|
173
|
-
//
|
|
191
|
+
// note: `headers` is already a copy of the original request/response content
|
|
174
192
|
if (headers && typeof headers === 'object') {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
maskedHeaders.authorization = '***MASKED***'
|
|
193
|
+
// mask sensitive headers
|
|
194
|
+
if (headers.authorization) {
|
|
195
|
+
headers.authorization = '***MASKED***'
|
|
179
196
|
}
|
|
180
|
-
if (
|
|
181
|
-
|
|
197
|
+
if (headers.cookie) {
|
|
198
|
+
headers.cookie = '***MASKED***'
|
|
182
199
|
}
|
|
183
|
-
return maskedHeaders
|
|
184
200
|
}
|
|
185
201
|
return headers
|
|
186
202
|
},
|
|
187
|
-
//
|
|
203
|
+
// list of field names to mask in request/response content
|
|
188
204
|
maskBodyFieldsList: ['password', 'token', 'secret'],
|
|
189
|
-
//
|
|
205
|
+
// list of headers to mask in request/response headers
|
|
190
206
|
maskHeadersList: ['authorization', 'cookie', 'x-api-key'],
|
|
191
|
-
//
|
|
207
|
+
// list of headers to capture. An empty array will capture all headers
|
|
192
208
|
headersToInclude: ['content-type', 'user-agent'],
|
|
193
|
-
//
|
|
209
|
+
// list of headers to exclude from capturing
|
|
194
210
|
headersToExclude: ['authorization', 'cookie']
|
|
195
|
-
}
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
SessionRecorder.setSessionAttributes({
|
|
199
|
-
userId: '12345',
|
|
200
|
-
userName: 'John Doe'
|
|
201
|
-
})
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
## API Methods
|
|
205
|
-
|
|
206
|
-
The Session Recorder provides several methods for controlling session recording:
|
|
207
|
-
|
|
208
|
-
### Session Control
|
|
209
|
-
|
|
210
|
-
- `SessionRecorder.start(type?, session?)` - Start a new session with optional existing session
|
|
211
|
-
- `type`: Optional `SessionType.PLAIN` or `SessionType.CONTINUOUS`, default: `SessionType.PLAIN`
|
|
212
|
-
- `session`: Optional existing session object
|
|
213
|
-
- `SessionRecorder.stop(comment?)` - Stop the current session with optional comment
|
|
214
|
-
- `SessionRecorder.pause()` - Pause the current session
|
|
215
|
-
- `SessionRecorder.resume()` - Resume the current session
|
|
216
|
-
- `SessionRecorder.cancel()` - Cancel the current session
|
|
217
|
-
- `SessionRecorder.save()` - Save the continuous recording session
|
|
218
|
-
|
|
219
|
-
### Configuration
|
|
220
|
-
|
|
221
|
-
- `SessionRecorder.setSessionAttributes(attributes)` - Set session metadata
|
|
222
|
-
- `SessionRecorder.recordingButtonClickHandler = handler` - Set custom click handler
|
|
223
|
-
|
|
224
|
-
### Properties
|
|
225
|
-
|
|
226
|
-
- `SessionRecorder.sessionId` - Get current session ID (readonly)
|
|
227
|
-
- `SessionRecorder.sessionType` - Get current session type (readonly)
|
|
228
|
-
- `SessionRecorder.sessionState` - Get current session state (readonly)
|
|
229
|
-
- `SessionRecorder.session` - Get current session object (readonly)
|
|
230
|
-
- `SessionRecorder.sessionAttributes` - Get current session attributes (readonly)
|
|
231
|
-
- `SessionRecorder.error` - Get/set error message
|
|
232
|
-
- `SessionRecorder.sessionWidgetButtonElement` - Get the widget button element (readonly)
|
|
233
|
-
|
|
234
|
-
### Session Types
|
|
235
|
-
|
|
236
|
-
- `SessionType.PLAIN` - Standard session recording
|
|
237
|
-
- `SessionType.CONTINUOUS` - Continuous recording session
|
|
238
|
-
|
|
239
|
-
### Session States
|
|
240
|
-
|
|
241
|
-
- `SessionState.started` - Session is currently recording
|
|
242
|
-
- `SessionState.paused` - Session is paused
|
|
243
|
-
- `SessionState.stopped` - Session is stopped
|
|
244
|
-
|
|
245
|
-
### Session Attributes
|
|
246
|
-
|
|
247
|
-
You can set various session attributes for better tracking:
|
|
248
|
-
|
|
249
|
-
```javascript
|
|
250
|
-
SessionRecorder.setSessionAttributes({
|
|
251
|
-
userId: '12345',
|
|
252
|
-
userName: 'John Doe',
|
|
253
|
-
userEmail: 'john@example.com',
|
|
254
|
-
accountId: 'acc_123',
|
|
255
|
-
accountName: 'Enterprise Account'
|
|
256
|
-
})
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
## Masking Configuration
|
|
260
|
-
|
|
261
|
-
The Session Recorder includes comprehensive masking options to protect sensitive data during session recordings. You can configure masking behavior through the `masking` option:
|
|
262
|
-
|
|
263
|
-
### Basic Masking Options
|
|
264
|
-
|
|
265
|
-
- `maskAllInputs`: If `true`, masks all input fields in the recording (default: `true`)
|
|
266
|
-
- `isMaskingEnabled`: If `true`, enables masking for debug span payload in traces (default: `true`)
|
|
267
|
-
|
|
268
|
-
### Input Type Masking
|
|
269
|
-
|
|
270
|
-
You can control masking for specific input types:
|
|
271
|
-
|
|
272
|
-
```javascript
|
|
273
|
-
maskInputOptions: {
|
|
274
|
-
password: true, // Always mask password fields (default: true)
|
|
275
|
-
email: false, // Don't mask email fields by default
|
|
276
|
-
tel: false, // Don't mask telephone fields by default
|
|
277
|
-
number: false, // Don't mask number fields by default
|
|
278
|
-
url: false, // Don't mask URL fields by default
|
|
279
|
-
search: false, // Don't mask search fields by default
|
|
280
|
-
textarea: false, // Don't mask textarea elements by default
|
|
281
|
-
select: false, // Don't mask select elements by default
|
|
282
|
-
// ...other types
|
|
283
|
-
}
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
### CSS Selector Masking
|
|
287
|
-
|
|
288
|
-
You can mask specific elements using CSS selectors:
|
|
289
|
-
|
|
290
|
-
```javascript
|
|
291
|
-
masking: {
|
|
292
|
-
// Mask text in elements matching this selector
|
|
293
|
-
maskTextSelector: '.sensitive-data, [data-private="true"], .user-profile .email',
|
|
294
|
-
}
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
### Class-Based Masking
|
|
298
|
-
|
|
299
|
-
You can mask text based on CSS classes using string or RegExp patterns:
|
|
300
|
-
|
|
301
|
-
```javascript
|
|
302
|
-
masking: {
|
|
303
|
-
maskTextClass: 'sensitive', // Mask text in elements with class 'sensitive'
|
|
304
|
-
}
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
Or with RegExp pattern:
|
|
308
|
-
|
|
309
|
-
```javascript
|
|
310
|
-
masking: {
|
|
311
|
-
maskTextClass: /private|confidential/, // Mask text in elements with classes 'private' or 'confidential'
|
|
312
|
-
}
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
### Custom Masking Functions
|
|
316
|
-
|
|
317
|
-
For advanced masking scenarios, you can provide custom functions:
|
|
318
|
-
|
|
319
|
-
```javascript
|
|
320
|
-
masking: {
|
|
321
|
-
// Custom function for input masking
|
|
322
|
-
maskInput: (text, element) => {
|
|
323
|
-
// Custom logic to mask input text
|
|
324
|
-
if (element.classList.contains('credit-card')) {
|
|
325
|
-
return '****-****-****-' + text.slice(-4);
|
|
326
|
-
}
|
|
327
|
-
return '***MASKED***';
|
|
328
|
-
},
|
|
329
|
-
|
|
330
|
-
// Custom function for text masking
|
|
331
|
-
maskText: (text, element) => {
|
|
332
|
-
// Custom logic to mask text content
|
|
333
|
-
if (element.dataset.type === 'email') {
|
|
334
|
-
const [local, domain] = text.split('@');
|
|
335
|
-
return local.charAt(0) + '***@' + domain;
|
|
336
|
-
}
|
|
337
|
-
return '***MASKED***';
|
|
338
|
-
},
|
|
339
|
-
|
|
340
|
-
// Custom function for masking body in traces
|
|
341
|
-
maskBody: (payload, span) => {
|
|
342
|
-
// Custom logic to mask sensitive data in trace payloads
|
|
343
|
-
if (payload && typeof payload === 'object') {
|
|
344
|
-
const maskedPayload = { ...payload };
|
|
345
|
-
// Mask sensitive fields
|
|
346
|
-
if (maskedPayload.headers) {
|
|
347
|
-
maskedPayload.headers = '***MASKED***';
|
|
348
|
-
}
|
|
349
|
-
if (maskedPayload.body) {
|
|
350
|
-
maskedPayload.body = '***MASKED***';
|
|
351
|
-
}
|
|
352
|
-
return maskedPayload;
|
|
353
|
-
}
|
|
354
|
-
return payload;
|
|
355
|
-
},
|
|
356
|
-
// Custom function for masking headers in traces
|
|
357
|
-
maskHeaders: (headers, span) => {
|
|
358
|
-
// Custom logic to mask sensitive headers
|
|
359
|
-
if (headers && typeof headers === 'object') {
|
|
360
|
-
const maskedHeaders = { ...headers };
|
|
361
|
-
// Mask sensitive headers
|
|
362
|
-
if (maskedHeaders.authorization) {
|
|
363
|
-
maskedHeaders.authorization = '***MASKED***';
|
|
364
|
-
}
|
|
365
|
-
if (maskedHeaders.cookie) {
|
|
366
|
-
maskedHeaders.cookie = '***MASKED***';
|
|
367
|
-
}
|
|
368
|
-
return maskedHeaders;
|
|
369
|
-
}
|
|
370
|
-
return headers;
|
|
371
211
|
},
|
|
372
|
-
}
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
### Example: Comprehensive Masking Setup
|
|
376
|
-
|
|
377
|
-
```javascript
|
|
378
|
-
SessionRecorder.init({
|
|
379
|
-
// ... other options
|
|
380
|
-
masking: {
|
|
381
|
-
maskAllInputs: true,
|
|
382
|
-
maskInputOptions: {
|
|
383
|
-
password: true,
|
|
384
|
-
email: true, // Mask email fields for privacy
|
|
385
|
-
tel: true, // Mask telephone fields for privacy
|
|
386
|
-
number: false, // Allow number fields
|
|
387
|
-
url: false, // Allow URL fields
|
|
388
|
-
search: false, // Allow search fields
|
|
389
|
-
textarea: false // Allow textarea elements
|
|
390
|
-
// ...other types
|
|
391
|
-
},
|
|
392
|
-
maskTextClass: /sensitive|private|confidential/, // Mask text in elements with these classes
|
|
393
|
-
maskTextSelector: '.user-email, .user-phone, .credit-card, [data-sensitive="true"]', // Mask text in elements matching this selector
|
|
394
|
-
maskInput: (text, element) => {
|
|
395
|
-
// Custom credit card masking
|
|
396
|
-
if (element.classList.contains('credit-card')) {
|
|
397
|
-
return '****-****-****-' + text.slice(-4)
|
|
398
|
-
}
|
|
399
|
-
return '***MASKED***'
|
|
400
|
-
},
|
|
401
|
-
maskText: (text, element) => {
|
|
402
|
-
// Custom email masking
|
|
403
|
-
if (element.dataset.type === 'email') {
|
|
404
|
-
const [local, domain] = text.split('@')
|
|
405
|
-
return local.charAt(0) + '***@' + domain
|
|
406
|
-
}
|
|
407
|
-
return '***MASKED***'
|
|
408
|
-
},
|
|
409
|
-
maskConsoleEvent: (payload) => {
|
|
410
|
-
// Custom console event masking
|
|
411
|
-
if (payload && payload.payload && payload.payload.args) {
|
|
412
|
-
payload.payload.args = payload.payload.args.map((arg) =>
|
|
413
|
-
typeof arg === 'string' && arg.includes('password') ? '***MASKED***' : arg
|
|
414
|
-
)
|
|
415
|
-
}
|
|
416
|
-
return payload
|
|
417
|
-
},
|
|
418
|
-
isMaskingEnabled: true, // Enable masking for debug span payload in traces
|
|
419
|
-
maskBody: (payload, span) => {
|
|
420
|
-
// Custom trace payload masking
|
|
421
|
-
if (payload && typeof payload === 'object') {
|
|
422
|
-
const maskedPayload = { ...payload }
|
|
423
|
-
// Mask sensitive trace data
|
|
424
|
-
if (maskedPayload.requestHeaders) {
|
|
425
|
-
maskedPayload.requestHeaders = '***MASKED***'
|
|
426
|
-
}
|
|
427
|
-
if (maskedPayload.responseBody) {
|
|
428
|
-
maskedPayload.responseBody = '***MASKED***'
|
|
429
|
-
}
|
|
430
|
-
return maskedPayload
|
|
431
|
-
}
|
|
432
|
-
return payload
|
|
433
|
-
},
|
|
434
|
-
maskHeaders: (headers, span) => {
|
|
435
|
-
// Custom headers masking
|
|
436
|
-
if (headers && typeof headers === 'object') {
|
|
437
|
-
const maskedHeaders = { ...headers }
|
|
438
|
-
// Mask sensitive headers
|
|
439
|
-
if (maskedHeaders.authorization) {
|
|
440
|
-
maskedHeaders.authorization = '***MASKED***'
|
|
441
|
-
}
|
|
442
|
-
if (maskedHeaders.cookie) {
|
|
443
|
-
maskedHeaders.cookie = '***MASKED***'
|
|
444
|
-
}
|
|
445
|
-
return maskedHeaders
|
|
446
|
-
},
|
|
447
|
-
// List of body fields to mask in traces
|
|
448
|
-
maskBodyFieldsList: ['password', 'token', 'secret'],
|
|
449
|
-
// List of headers to mask in traces
|
|
450
|
-
maskHeadersList: ['authorization', 'cookie', 'x-api-key'],
|
|
451
|
-
// List of headers to include in traces (if specified, only these headers will be captured)
|
|
452
|
-
headersToInclude: ['content-type', 'user-agent'],
|
|
453
|
-
// List of headers to exclude from traces
|
|
454
|
-
headersToExclude: ['authorization', 'cookie']
|
|
455
|
-
}
|
|
456
|
-
})
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
## Session Recorder for Next.js
|
|
460
|
-
|
|
461
|
-
To integrate the MySessionRecorder component into your Next.js application, follow these steps:
|
|
462
|
-
|
|
463
|
-
- Create a new file (e.g., MySessionRecorder.js or MySessionRecorder.tsx) in your root directory or a components directory.
|
|
464
|
-
|
|
465
|
-
- Import the component
|
|
466
|
-
|
|
467
|
-
In the newly created file, add the following code:
|
|
468
|
-
|
|
469
|
-
```javascript
|
|
470
|
-
'use client' // Mark as Client Component
|
|
471
|
-
import { useEffect } from 'react'
|
|
472
|
-
import SessionRecorder from '@multiplayer-app/session-recorder-browser'
|
|
473
|
-
|
|
474
|
-
export default function MySessionRecorder() {
|
|
475
|
-
useEffect(() => {
|
|
476
|
-
if (typeof window !== 'undefined') {
|
|
477
|
-
SessionRecorder.init({
|
|
478
|
-
version: '{YOUR_APPLICATION_VERSION}',
|
|
479
|
-
application: '{YOUR_APPLICATION_NAME}',
|
|
480
|
-
environment: '{YOUR_APPLICATION_ENVIRONMENT}',
|
|
481
|
-
apiKey: '{YOUR_API_KEY}',
|
|
482
|
-
recordCanvas: true, // Enable canvas recording
|
|
483
|
-
masking: {
|
|
484
|
-
maskAllInputs: true,
|
|
485
|
-
maskInputOptions: {
|
|
486
|
-
password: true,
|
|
487
|
-
email: false,
|
|
488
|
-
tel: false
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
})
|
|
492
|
-
|
|
493
|
-
SessionRecorder.setSessionAttributes({
|
|
494
|
-
userId: '{userId}',
|
|
495
|
-
userName: '{userName}'
|
|
496
|
-
})
|
|
497
|
-
}
|
|
498
|
-
}, [])
|
|
499
|
-
|
|
500
|
-
return null // No UI output needed
|
|
501
|
-
}
|
|
502
|
-
```
|
|
503
212
|
|
|
504
|
-
|
|
213
|
+
// advanced options
|
|
505
214
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
```javascript
|
|
509
|
-
import MySessionRecorder from './MySessionRecorder' // Adjust the path as necessary
|
|
510
|
-
|
|
511
|
-
export default function MyApp() {
|
|
512
|
-
return (
|
|
513
|
-
<>
|
|
514
|
-
<MySessionRecorder />
|
|
515
|
-
{/* Other components */}
|
|
516
|
-
</>
|
|
517
|
-
)
|
|
518
|
-
}
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
## Note
|
|
522
|
-
|
|
523
|
-
If frontend domain doesn't match to backend one, set backend domain to `propagateTraceHeaderCorsUrls` parameter:
|
|
524
|
-
|
|
525
|
-
```javascript
|
|
526
|
-
import SessionRecorder from '@multiplayer-app/session-recorder-browser'
|
|
527
|
-
|
|
528
|
-
SessionRecorder.init({
|
|
529
|
-
version: '{YOUR_APPLICATION_VERSION}',
|
|
530
|
-
application: '{YOUR_APPLICATION_NAME}',
|
|
531
|
-
environment: '{YOUR_APPLICATION_ENVIRONMENT}',
|
|
532
|
-
apiKey: '{YOUR_API_KEY}',
|
|
533
|
-
propagateTraceHeaderCorsUrls: new RegExp(`https://your.backend.api.domain`, 'i')
|
|
215
|
+
// allow Multiplayer Chrome to use post messages through the library
|
|
216
|
+
usePostMessageFallback: false
|
|
534
217
|
})
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
If frontend sends api requests to two or more different domains put them to `propagateTraceHeaderCorsUrls` as array:
|
|
538
|
-
|
|
539
|
-
```javascript
|
|
540
|
-
import SessionRecorder from '@multiplayer-app/session-recorder-browser'
|
|
541
218
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
apiKey: '{YOUR_API_KEY}',
|
|
547
|
-
propagateTraceHeaderCorsUrls: [
|
|
548
|
-
new RegExp(`https://your.backend.api.domain`, 'i'),
|
|
549
|
-
new RegExp(`https://another.backend.api.domain`, 'i')
|
|
550
|
-
]
|
|
219
|
+
// add any key value pairs which should be associated with a session
|
|
220
|
+
SessionRecorder.setSessionAttributes({
|
|
221
|
+
userId: '12345',
|
|
222
|
+
userName: 'John Doe'
|
|
551
223
|
})
|
|
552
224
|
```
|
|
553
225
|
|
|
554
|
-
|
|
226
|
+
### Framework notes
|
|
555
227
|
|
|
556
|
-
|
|
228
|
+
- Next.js: initialize the browser SDK in a Client Component (see example in the browser README). Ensure it runs only in the browser.
|
|
229
|
+
- CORS: when your frontend calls multiple API domains, set `propagateTraceHeaderCorsUrls` to match them so parent/child spans correlate across services.
|
|
557
230
|
|
|
558
231
|
## License
|
|
559
232
|
|
|
560
|
-
|
|
233
|
+
MIT — see [LICENSE](./LICENSE).
|