@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/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 truncateContent;
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.truncateContent(requestBody, self.networkSettings.maxRequestBodySize);
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.truncateContent(text, self.networkSettings.maxResponseBodySize);
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.truncateContent(requestBody, self.networkSettings.maxRequestBodySize),
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.truncateContent(xhr.responseText, self.networkSettings.maxResponseBodySize);
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
- truncateContent(content, maxSize) {
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
- processedContent = this.networkSettings.requestBodyMaskingFunction(content);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@affogatosoftware/recorder",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "license": "MIT",
5
5
  "author": "Chris Ryan",
6
6
  "main": "./dist/index.js",