@monoscopetech/browser 0.5.5 → 0.5.6

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/replay.d.ts CHANGED
@@ -1,9 +1,23 @@
1
1
  import { MonoscopeConfig } from "./types";
2
2
  export declare class MonoscopeReplay {
3
- events: any[];
4
- config: MonoscopeConfig;
5
- sessionId: string;
3
+ private events;
4
+ private config;
5
+ private sessionId;
6
+ private stopRecording;
7
+ private saveInterval;
8
+ private isSaving;
9
+ private isConfigured;
6
10
  constructor(config: MonoscopeConfig, sessionId: string);
11
+ private setupEventListeners;
12
+ private handleUnload;
13
+ private handleVisibilityChange;
7
14
  configure(): void;
8
- save(): void;
15
+ save(forceSynchronous?: boolean): Promise<void>;
16
+ /**
17
+ * Stop recording and clean up
18
+ */
19
+ stop(): void;
20
+ getEventCount(): number;
21
+ getSessionId(): string;
22
+ isRecording(): boolean;
9
23
  }
package/dist/replay.js CHANGED
@@ -1,72 +1,205 @@
1
1
  import { getRecordConsolePlugin } from "@rrweb/rrweb-plugin-console-record";
2
2
  import * as rrweb from "rrweb";
3
- const MAX_EVENT_BATCH = 5;
3
+ const MAX_EVENT_BATCH = 50;
4
+ const SAVE_INTERVAL = 10000;
5
+ const MAX_RETRY_EVENTS = 1000;
4
6
  export class MonoscopeReplay {
5
7
  constructor(config, sessionId) {
6
8
  this.events = [];
9
+ this.stopRecording = undefined;
10
+ this.saveInterval = null;
11
+ this.isSaving = false;
12
+ this.isConfigured = false;
7
13
  this.sessionId = sessionId;
8
14
  this.config = config;
9
- this.save = this.save.bind(this);
10
15
  this.events = [];
16
+ // Bind methods
17
+ this.save = this.save.bind(this);
11
18
  this.configure = this.configure.bind(this);
12
- window.addEventListener("unload", () => this.save());
19
+ this.handleUnload = this.handleUnload.bind(this);
20
+ this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
21
+ // Setup event listeners
22
+ this.setupEventListeners();
23
+ }
24
+ setupEventListeners() {
25
+ window.addEventListener("beforeunload", this.handleUnload);
26
+ document.addEventListener("visibilitychange", this.handleVisibilityChange);
27
+ window.addEventListener("pagehide", this.handleUnload);
28
+ }
29
+ handleUnload() {
30
+ this.save(true); // Force synchronous save on unload
31
+ }
32
+ handleVisibilityChange() {
33
+ if (document.visibilityState === "hidden") {
34
+ this.save();
35
+ }
13
36
  }
14
37
  configure() {
15
- rrweb.record({
16
- emit: (event) => {
17
- this.events.push(event);
18
- if (this.events.length >= MAX_EVENT_BATCH) {
19
- this.save();
20
- }
21
- },
22
- checkoutEveryNms: 10 * 1000,
23
- checkoutEveryNth: 10,
24
- sampling: {
25
- mouseInteraction: false,
26
- scroll: 150,
27
- media: 800,
28
- input: "last",
29
- },
30
- plugins: [
31
- getRecordConsolePlugin({
32
- level: ["info", "log", "warn", "error"],
33
- lengthThreshold: 10000,
34
- stringifyOptions: {
35
- stringLengthLimit: 1000,
36
- numOfKeysLimit: 100,
37
- depthOfLimit: 1,
38
+ if (this.isConfigured) {
39
+ console.warn("MonoscopeReplay already configured");
40
+ return;
41
+ }
42
+ try {
43
+ this.stopRecording = rrweb.record({
44
+ emit: (event) => {
45
+ // Don't record if we're in a weird state
46
+ if (!event || typeof event !== "object") {
47
+ console.warn("Invalid event received:", event);
48
+ return;
49
+ }
50
+ this.events.push(event);
51
+ // Auto-save when batch size reached
52
+ if (this.events.length >= MAX_EVENT_BATCH) {
53
+ this.save();
54
+ }
55
+ },
56
+ // Prevent recording replay UI elements
57
+ blockClass: "rr-block",
58
+ blockSelector: "#replay-container, .replay-ui, [data-rrweb-ignore]",
59
+ // Privacy settings
60
+ maskAllInputs: true,
61
+ maskInputOptions: {
62
+ password: true,
63
+ email: true,
64
+ tel: true,
65
+ },
66
+ maskTextClass: "rr-mask",
67
+ // Performance settings
68
+ checkoutEveryNms: 15 * 1000, // Full snapshot every 15s
69
+ sampling: {
70
+ mouseInteraction: {
71
+ MouseUp: false,
72
+ MouseDown: false,
73
+ Click: true,
74
+ ContextMenu: false,
75
+ DblClick: true,
76
+ Focus: false,
77
+ Blur: false,
78
+ TouchStart: true,
79
+ TouchEnd: false,
38
80
  },
39
- }),
40
- ],
41
- });
42
- setInterval(this.save, 5 * 1000);
81
+ mousemove: true,
82
+ scroll: 150, // Throttle scroll events
83
+ media: 800,
84
+ input: "last", // Only capture final input value
85
+ },
86
+ // Console plugin
87
+ plugins: [
88
+ getRecordConsolePlugin({
89
+ level: ["info", "log", "warn", "error"],
90
+ lengthThreshold: 10000,
91
+ stringifyOptions: {
92
+ stringLengthLimit: 1000,
93
+ numOfKeysLimit: 100,
94
+ depthOfLimit: 2, // Increased from 1 for better context
95
+ },
96
+ }),
97
+ ],
98
+ });
99
+ // Setup periodic save
100
+ this.saveInterval = setInterval(() => {
101
+ this.save();
102
+ }, SAVE_INTERVAL);
103
+ this.isConfigured = true;
104
+ console.log("MonoscopeReplay configured successfully");
105
+ }
106
+ catch (error) {
107
+ console.error("Failed to configure MonoscopeReplay:", error);
108
+ throw error;
109
+ }
43
110
  }
44
- save() {
45
- if (this.events.length === 0)
111
+ async save(forceSynchronous = false) {
112
+ // Prevent concurrent saves
113
+ if (this.isSaving && !forceSynchronous) {
114
+ return;
115
+ }
116
+ // Nothing to save
117
+ if (this.events.length === 0) {
46
118
  return;
47
- let { replayEventsBaseUrl, projectId } = this.config;
48
- if (!replayEventsBaseUrl) {
49
- replayEventsBaseUrl = `https://app.apitoolkit.io/rrweb/${projectId}`;
50
119
  }
51
- else {
52
- replayEventsBaseUrl = `${replayEventsBaseUrl}/rrweb/${projectId}`;
120
+ // Prevent event array from growing too large during failed saves
121
+ if (this.events.length > MAX_RETRY_EVENTS) {
122
+ console.warn(`Event queue exceeded ${MAX_RETRY_EVENTS}, dropping oldest events`);
123
+ this.events = this.events.slice(-MAX_RETRY_EVENTS);
53
124
  }
54
- const events = this.events;
125
+ this.isSaving = true;
126
+ const { replayEventsBaseUrl, projectId } = this.config;
127
+ // Construct base URL
128
+ let baseUrl = replayEventsBaseUrl || "https://app.monoscope.tech";
129
+ baseUrl = `${baseUrl}/rrweb/${projectId}`;
130
+ // Get events to send and clear buffer
131
+ const eventsToSend = [...this.events];
55
132
  this.events = [];
56
- const body = JSON.stringify({
57
- events,
133
+ const payload = {
134
+ events: eventsToSend,
58
135
  sessionId: this.sessionId,
59
136
  timestamp: new Date().toISOString(),
60
- });
61
- fetch(replayEventsBaseUrl, {
62
- method: "POST",
63
- headers: {
64
- "Content-Type": "application/json",
65
- },
66
- body,
67
- }).catch((error) => {
137
+ eventCount: eventsToSend.length,
138
+ };
139
+ try {
140
+ if (forceSynchronous && navigator.sendBeacon) {
141
+ // Use sendBeacon for unload events (more reliable)
142
+ const blob = new Blob([JSON.stringify(payload)], {
143
+ type: "application/json",
144
+ });
145
+ const sent = navigator.sendBeacon(baseUrl, blob);
146
+ if (!sent) {
147
+ console.warn("sendBeacon failed, events may be lost");
148
+ }
149
+ }
150
+ else {
151
+ // Regular fetch with keepalive
152
+ const response = await fetch(baseUrl, {
153
+ method: "POST",
154
+ headers: {
155
+ "Content-Type": "application/json",
156
+ },
157
+ body: JSON.stringify(payload),
158
+ keepalive: true, // Important for unload scenarios
159
+ });
160
+ if (!response.ok) {
161
+ throw new Error(`Failed to save replay events: ${response.status} ${response.statusText}`);
162
+ }
163
+ console.log(`Successfully saved ${eventsToSend.length} replay events`);
164
+ }
165
+ }
166
+ catch (error) {
68
167
  console.error("Failed to save replay events:", error);
69
- this.events = [...events, ...this.events];
70
- });
168
+ this.events = [...eventsToSend, ...this.events];
169
+ if (this.events.length > MAX_RETRY_EVENTS) {
170
+ this.events = this.events.slice(-MAX_RETRY_EVENTS);
171
+ }
172
+ }
173
+ finally {
174
+ this.isSaving = false;
175
+ }
176
+ }
177
+ /**
178
+ * Stop recording and clean up
179
+ */
180
+ stop() {
181
+ this.save(true);
182
+ if (this.stopRecording) {
183
+ this.stopRecording();
184
+ this.stopRecording = undefined;
185
+ }
186
+ if (this.saveInterval) {
187
+ clearInterval(this.saveInterval);
188
+ this.saveInterval = null;
189
+ }
190
+ window.removeEventListener("beforeunload", this.handleUnload);
191
+ window.removeEventListener("pagehide", this.handleUnload);
192
+ document.removeEventListener("visibilitychange", this.handleVisibilityChange);
193
+ this.isConfigured = false;
194
+ console.log("MonoscopeReplay stopped");
195
+ }
196
+ getEventCount() {
197
+ return this.events.length;
198
+ }
199
+ getSessionId() {
200
+ return this.sessionId;
201
+ }
202
+ isRecording() {
203
+ return this.isConfigured && this.stopRecording !== null;
71
204
  }
72
205
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoscopetech/browser",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",