@affogatosoftware/recorder 1.2.0 → 1.4.0
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/README.md +28 -1
- package/dist/browser/recorder.iife.js +2 -2
- package/dist/browser/recorder.iife.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/recorder/networkRecorder.d.ts +3 -1
- package/dist/recorder/networkRecorder.js +30 -7
- package/dist/recorder/recorder.d.ts +14 -0
- package/dist/recorder/recorder.js +40 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { Recorder, type RecorderSettings } from './recorder/recorder';
|
|
1
|
+
export { Recorder, type RecorderSettings, type CapturedUserIdentity } from './recorder/recorder';
|
|
2
2
|
export { NetworkRecorder, type NetworkRequest } from './recorder/networkRecorder';
|
|
@@ -22,6 +22,7 @@ interface NetworkRecorderSettings {
|
|
|
22
22
|
captureResponseBodies: boolean;
|
|
23
23
|
excludeHeaders: string[];
|
|
24
24
|
requestBodyMaskingFunction?: (body: string) => string;
|
|
25
|
+
responseBodyMaskingFunction?: (body: string) => string;
|
|
25
26
|
}
|
|
26
27
|
export declare class NetworkRecorder {
|
|
27
28
|
private window;
|
|
@@ -47,7 +48,8 @@ export declare class NetworkRecorder {
|
|
|
47
48
|
private shouldCaptureRequest;
|
|
48
49
|
private sanitizeUrl;
|
|
49
50
|
private filterHeaders;
|
|
50
|
-
private
|
|
51
|
+
private truncateRequestContent;
|
|
52
|
+
private truncateResponseContent;
|
|
51
53
|
private generateRequestId;
|
|
52
54
|
private addCompletedRequest;
|
|
53
55
|
private isErrorRequest;
|
|
@@ -152,7 +152,7 @@ export class NetworkRecorder {
|
|
|
152
152
|
}
|
|
153
153
|
const sanitizedUrl = self.sanitizeUrl(url);
|
|
154
154
|
const filteredHeaders = self.filterHeaders(requestHeaders);
|
|
155
|
-
const truncatedBody = self.
|
|
155
|
+
const truncatedBody = self.truncateRequestContent(requestBody, self.networkSettings.maxRequestBodySize);
|
|
156
156
|
// Create initial request record
|
|
157
157
|
const networkRequest = {
|
|
158
158
|
requestId,
|
|
@@ -178,7 +178,7 @@ export class NetworkRecorder {
|
|
|
178
178
|
try {
|
|
179
179
|
const clonedResponse = response.clone();
|
|
180
180
|
const text = await clonedResponse.text();
|
|
181
|
-
responseBody = self.
|
|
181
|
+
responseBody = self.truncateResponseContent(text, self.networkSettings.maxResponseBodySize);
|
|
182
182
|
}
|
|
183
183
|
catch (e) {
|
|
184
184
|
responseBody = "[Unable to read response body]";
|
|
@@ -249,7 +249,7 @@ export class NetworkRecorder {
|
|
|
249
249
|
method,
|
|
250
250
|
url,
|
|
251
251
|
requestHeaders: Object.keys(requestHeaders).length > 0 ? self.filterHeaders(requestHeaders) : undefined,
|
|
252
|
-
requestBody: self.
|
|
252
|
+
requestBody: self.truncateRequestContent(requestBody, self.networkSettings.maxRequestBodySize),
|
|
253
253
|
timestamp
|
|
254
254
|
};
|
|
255
255
|
self.pendingRequests.set(requestId, networkRequest);
|
|
@@ -273,7 +273,7 @@ export class NetworkRecorder {
|
|
|
273
273
|
}
|
|
274
274
|
if (self.networkSettings.captureResponseBodies) {
|
|
275
275
|
try {
|
|
276
|
-
responseBody = self.
|
|
276
|
+
responseBody = self.truncateResponseContent(xhr.responseText, self.networkSettings.maxResponseBodySize);
|
|
277
277
|
}
|
|
278
278
|
catch (e) {
|
|
279
279
|
responseBody = "[Unable to read response]";
|
|
@@ -353,13 +353,36 @@ export class NetworkRecorder {
|
|
|
353
353
|
}
|
|
354
354
|
return filtered;
|
|
355
355
|
}
|
|
356
|
-
|
|
356
|
+
truncateRequestContent(content, maxSize) {
|
|
357
357
|
if (!content)
|
|
358
358
|
return content;
|
|
359
|
-
// Apply masking function if provided
|
|
359
|
+
// Apply request masking function if provided
|
|
360
360
|
let processedContent = content;
|
|
361
361
|
if (this.networkSettings.requestBodyMaskingFunction) {
|
|
362
|
-
|
|
362
|
+
try {
|
|
363
|
+
processedContent = this.networkSettings.requestBodyMaskingFunction(content);
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
logger.error("Failed to apply request masking function");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (processedContent.length > maxSize) {
|
|
370
|
+
return processedContent.substring(0, maxSize) + `... [truncated from ${processedContent.length} chars]`;
|
|
371
|
+
}
|
|
372
|
+
return processedContent;
|
|
373
|
+
}
|
|
374
|
+
truncateResponseContent(content, maxSize) {
|
|
375
|
+
if (!content)
|
|
376
|
+
return content;
|
|
377
|
+
// Apply response masking function if provided
|
|
378
|
+
let processedContent = content;
|
|
379
|
+
if (this.networkSettings.responseBodyMaskingFunction) {
|
|
380
|
+
try {
|
|
381
|
+
processedContent = this.networkSettings.responseBodyMaskingFunction(content);
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
logger.error("Failed to apply response masking function");
|
|
385
|
+
}
|
|
363
386
|
}
|
|
364
387
|
if (processedContent.length > maxSize) {
|
|
365
388
|
return processedContent.substring(0, maxSize) + `... [truncated from ${processedContent.length} chars]`;
|
|
@@ -9,6 +9,7 @@ export declare class Recorder {
|
|
|
9
9
|
private capturedSessionId;
|
|
10
10
|
private pingIntervalMs;
|
|
11
11
|
private pingTimeout;
|
|
12
|
+
private userIdentity;
|
|
12
13
|
constructor(window: Window, publicToken: string, userSettings?: Partial<RecorderSettings>);
|
|
13
14
|
private schedulePing;
|
|
14
15
|
private ping;
|
|
@@ -20,6 +21,15 @@ export declare class Recorder {
|
|
|
20
21
|
* Stop all recorders
|
|
21
22
|
*/
|
|
22
23
|
stop(): void;
|
|
24
|
+
/**
|
|
25
|
+
* Identify the current user with a unique ID
|
|
26
|
+
* @param userId - Unique identifier for the user (e.g., database ID, email)
|
|
27
|
+
* @example
|
|
28
|
+
* recorder.identify('user_123');
|
|
29
|
+
*/
|
|
30
|
+
identify(userId: string): void;
|
|
31
|
+
clearUserIdentity(): void;
|
|
32
|
+
private sendUserIdentification;
|
|
23
33
|
private collectCapturedUserMetadata;
|
|
24
34
|
}
|
|
25
35
|
export interface RecorderSettings {
|
|
@@ -37,6 +47,7 @@ export interface RecorderSettings {
|
|
|
37
47
|
captureResponseBodies: boolean;
|
|
38
48
|
excludeHeaders: string[];
|
|
39
49
|
requestBodyMaskingFunction?: (body: string) => string;
|
|
50
|
+
responseBodyMaskingFunction?: (body: string) => string;
|
|
40
51
|
};
|
|
41
52
|
}
|
|
42
53
|
export interface CapturedUserMetadata {
|
|
@@ -58,3 +69,6 @@ export interface CapturedUserMetadata {
|
|
|
58
69
|
utmContent?: string;
|
|
59
70
|
utmTerm?: string;
|
|
60
71
|
}
|
|
72
|
+
export interface CapturedUserIdentity {
|
|
73
|
+
userId: string;
|
|
74
|
+
}
|
|
@@ -2,7 +2,7 @@ import { SessionRecorder } from "./sessionRecorder";
|
|
|
2
2
|
import { EventRecorder } from "./eventRecorder";
|
|
3
3
|
import { ErrorRecorder } from "./errorRecorder";
|
|
4
4
|
import { NetworkRecorder } from "./networkRecorder";
|
|
5
|
-
import { post, put } from "../requests";
|
|
5
|
+
import { post, put, patch } from "../requests";
|
|
6
6
|
import { UAParser } from "ua-parser-js";
|
|
7
7
|
export class Recorder {
|
|
8
8
|
window;
|
|
@@ -15,6 +15,7 @@ export class Recorder {
|
|
|
15
15
|
capturedSessionId = null;
|
|
16
16
|
pingIntervalMs = 20000;
|
|
17
17
|
pingTimeout = null;
|
|
18
|
+
userIdentity = null;
|
|
18
19
|
constructor(window, publicToken, userSettings = {}) {
|
|
19
20
|
this.window = window;
|
|
20
21
|
this.publicToken = publicToken;
|
|
@@ -61,6 +62,8 @@ export class Recorder {
|
|
|
61
62
|
this.schedulePing();
|
|
62
63
|
const capturedUserMetadata = this.collectCapturedUserMetadata();
|
|
63
64
|
post(`public/captured-sessions/${this.capturedSessionId}/captured-session/metadata`, capturedUserMetadata, { withCredentials: false });
|
|
65
|
+
// Send user identification if it was set before session creation
|
|
66
|
+
this.sendUserIdentification(this.capturedSessionId, this.userIdentity);
|
|
64
67
|
})
|
|
65
68
|
.catch(error => {
|
|
66
69
|
console.error(error);
|
|
@@ -98,6 +101,42 @@ export class Recorder {
|
|
|
98
101
|
this.errorRecorder.stop();
|
|
99
102
|
this.networkRecorder.stop();
|
|
100
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Identify the current user with a unique ID
|
|
106
|
+
* @param userId - Unique identifier for the user (e.g., database ID, email)
|
|
107
|
+
* @example
|
|
108
|
+
* recorder.identify('user_123');
|
|
109
|
+
*/
|
|
110
|
+
identify(userId) {
|
|
111
|
+
if (!userId || userId.trim() === '') {
|
|
112
|
+
console.error('Recorder.identify: userId must be a non-empty string');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.userIdentity = {
|
|
116
|
+
userId: userId.trim()
|
|
117
|
+
};
|
|
118
|
+
// If session is already created, send identification immediately
|
|
119
|
+
this.sendUserIdentification(this.capturedSessionId, this.userIdentity);
|
|
120
|
+
// If not, identification will be sent when session is created
|
|
121
|
+
}
|
|
122
|
+
clearUserIdentity() {
|
|
123
|
+
this.userIdentity = null;
|
|
124
|
+
}
|
|
125
|
+
async sendUserIdentification(capturedSessionId, userIdentity) {
|
|
126
|
+
if (!capturedSessionId || !userIdentity) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const response = await patch(`public/captured-sessions/${capturedSessionId}/identify`, userIdentity.userId, { withCredentials: false });
|
|
131
|
+
if (response.status >= 400) {
|
|
132
|
+
console.error(`Failed to identify user: HTTP ${response.status}`, response.data);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.error("Error sending user identification:", error);
|
|
137
|
+
// Identification failure is non-critical - recording should continue
|
|
138
|
+
}
|
|
139
|
+
}
|
|
101
140
|
collectCapturedUserMetadata = () => {
|
|
102
141
|
const ua = new UAParser();
|
|
103
142
|
const browserName = ua.getBrowser().name;
|