@monoscopetech/browser 0.6.0 → 0.7.1

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.
@@ -0,0 +1,28 @@
1
+ import { Component } from "react";
2
+ import type { ReactNode, ErrorInfo } from "react";
3
+ import Monoscope from ".";
4
+ import type { MonoscopeConfig, MonoscopeUser } from "./types";
5
+ export declare function MonoscopeProvider({ config, children }: {
6
+ config: MonoscopeConfig;
7
+ children: ReactNode;
8
+ }): import("react/jsx-runtime").JSX.Element;
9
+ export declare function useMonoscope(): Monoscope | null;
10
+ export declare function useMonoscopeUser(user: MonoscopeUser | null | undefined): void;
11
+ type ErrorBoundaryProps = {
12
+ children: ReactNode;
13
+ fallback?: ReactNode | ((error: Error) => ReactNode);
14
+ };
15
+ type ErrorBoundaryState = {
16
+ error: Error | null;
17
+ };
18
+ export declare class MonoscopeErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
19
+ static contextType: import("react").Context<Monoscope | null>;
20
+ context: Monoscope | null;
21
+ state: ErrorBoundaryState;
22
+ static getDerivedStateFromError(error: Error): {
23
+ error: Error;
24
+ };
25
+ componentDidCatch(error: Error, info: ErrorInfo): void;
26
+ render(): ReactNode;
27
+ }
28
+ export {};
package/dist/react.js ADDED
@@ -0,0 +1,59 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext, useRef, useEffect, Component } from "react";
4
+ import Monoscope from ".";
5
+ const MonoscopeContext = createContext(null);
6
+ export function MonoscopeProvider({ config, children }) {
7
+ const ref = useRef(null);
8
+ if (!ref.current && typeof window !== "undefined") {
9
+ ref.current = new Monoscope(config);
10
+ }
11
+ useEffect(() => {
12
+ const instance = ref.current;
13
+ if (!instance)
14
+ return;
15
+ // Deferred destroy — cleared if Strict Mode remounts immediately
16
+ let timer;
17
+ return () => {
18
+ timer = setTimeout(() => { instance.destroy(); ref.current = null; }, 0);
19
+ return void timer;
20
+ };
21
+ }, []);
22
+ return _jsx(MonoscopeContext.Provider, { value: ref.current, children: children });
23
+ }
24
+ export function useMonoscope() {
25
+ return useContext(MonoscopeContext);
26
+ }
27
+ export function useMonoscopeUser(user) {
28
+ const instance = useMonoscope();
29
+ useEffect(() => {
30
+ if (instance && user)
31
+ instance.setUser(user);
32
+ }, [instance, user]);
33
+ }
34
+ export class MonoscopeErrorBoundary extends Component {
35
+ constructor() {
36
+ super(...arguments);
37
+ this.state = { error: null };
38
+ }
39
+ static getDerivedStateFromError(error) {
40
+ return { error };
41
+ }
42
+ componentDidCatch(error, info) {
43
+ this.context?.recordEvent("react.error_boundary", {
44
+ "error.message": error.message,
45
+ "error.stack": error.stack ?? "",
46
+ "error.component_stack": info.componentStack ?? "",
47
+ });
48
+ }
49
+ render() {
50
+ if (this.state.error) {
51
+ const { fallback } = this.props;
52
+ if (typeof fallback === "function")
53
+ return fallback(this.state.error);
54
+ return fallback ?? null;
55
+ }
56
+ return this.props.children;
57
+ }
58
+ }
59
+ MonoscopeErrorBoundary.contextType = MonoscopeContext;
package/dist/replay.d.ts CHANGED
@@ -3,18 +3,27 @@ export declare class MonoscopeReplay {
3
3
  private events;
4
4
  private config;
5
5
  private sessionId;
6
+ private tabId;
6
7
  private stopRecording;
7
8
  private saveInterval;
8
9
  private isSaving;
9
10
  private isConfigured;
10
- constructor(config: MonoscopeConfig, sessionId: string);
11
- private setupEventListeners;
11
+ private _enabled;
12
+ private userAttributes;
13
+ private _listenersAttached;
12
14
  private handleUnload;
13
15
  private handleVisibilityChange;
16
+ constructor(config: MonoscopeConfig, sessionId: string, tabId: string);
17
+ private setupListeners;
18
+ private removeListeners;
19
+ private trimEvents;
14
20
  configure(): void;
15
21
  save(forceSynchronous?: boolean): Promise<void>;
16
22
  stop(): void;
23
+ setEnabled(enabled: boolean): void;
24
+ setUser(user: Record<string, string | string[] | undefined>): void;
17
25
  getEventCount(): number;
26
+ updateSessionId(sessionId: string): void;
18
27
  getSessionId(): string;
19
28
  isRecording(): boolean;
20
29
  }
package/dist/replay.js CHANGED
@@ -4,46 +4,71 @@ const MAX_EVENT_BATCH = 50;
4
4
  const SAVE_INTERVAL = 2000;
5
5
  const MAX_RETRY_EVENTS = 5000;
6
6
  export class MonoscopeReplay {
7
- constructor(config, sessionId) {
7
+ constructor(config, sessionId, tabId) {
8
8
  this.events = [];
9
9
  this.stopRecording = undefined;
10
10
  this.saveInterval = null;
11
11
  this.isSaving = false;
12
12
  this.isConfigured = false;
13
+ this._enabled = true;
14
+ this.userAttributes = {};
15
+ this._listenersAttached = false;
16
+ this.handleUnload = () => this.save(true);
17
+ this.handleVisibilityChange = () => { if (document.visibilityState === "hidden")
18
+ this.save(); };
13
19
  this.sessionId = sessionId;
20
+ this.tabId = tabId;
14
21
  this.config = config;
15
- this.events = [];
16
- // Bind methods
17
- this.save = this.save.bind(this);
18
- this.configure = this.configure.bind(this);
19
- this.handleUnload = this.handleUnload.bind(this);
20
- this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
21
- // Setup event listeners
22
- this.setupEventListeners();
22
+ this.setupListeners();
23
23
  }
24
- setupEventListeners() {
24
+ setupListeners() {
25
+ if (typeof window === "undefined" || this._listenersAttached)
26
+ return;
27
+ this._listenersAttached = true;
25
28
  window.addEventListener("beforeunload", this.handleUnload);
26
- document.addEventListener("visibilitychange", this.handleVisibilityChange);
27
29
  window.addEventListener("pagehide", this.handleUnload);
30
+ document.addEventListener("visibilitychange", this.handleVisibilityChange);
28
31
  }
29
- handleUnload() {
30
- this.save(true); // Force synchronous save on unload
32
+ removeListeners() {
33
+ if (typeof window === "undefined" || !this._listenersAttached)
34
+ return;
35
+ this._listenersAttached = false;
36
+ window.removeEventListener("beforeunload", this.handleUnload);
37
+ window.removeEventListener("pagehide", this.handleUnload);
38
+ document.removeEventListener("visibilitychange", this.handleVisibilityChange);
31
39
  }
32
- handleVisibilityChange() {
33
- if (document.visibilityState === "hidden") {
34
- this.save();
40
+ trimEvents() {
41
+ if (this.events.length <= MAX_RETRY_EVENTS)
42
+ return;
43
+ if (this.config.debug) {
44
+ console.warn(`Event queue exceeded ${MAX_RETRY_EVENTS}, dropping middle events (preserving snapshots)`);
35
45
  }
46
+ // rrweb EventType.FullSnapshot (type 2) — required for replay playback
47
+ const fullSnapshots = this.events.filter((e) => e.type === 2);
48
+ const otherEvents = this.events.filter((e) => e.type !== 2);
49
+ const remainingSlots = Math.max(0, MAX_RETRY_EVENTS - fullSnapshots.length);
50
+ this.events = [...fullSnapshots, ...otherEvents.slice(-remainingSlots)];
51
+ this.events.sort((a, b) => a.timestamp - b.timestamp);
36
52
  }
37
53
  configure() {
38
- if (this.isConfigured) {
39
- console.warn("MonoscopeReplay already configured");
54
+ if (typeof window === "undefined")
55
+ return;
56
+ if (this.isConfigured)
57
+ return;
58
+ this.setupListeners();
59
+ const rate = Math.max(0, Math.min(1, this.config.replaySampleRate ?? 1));
60
+ if (Math.random() >= rate) {
61
+ this._enabled = false;
62
+ if (this.config.debug)
63
+ console.log("MonoscopeReplay: sampled out");
40
64
  return;
41
65
  }
42
66
  try {
43
67
  this.stopRecording = rrweb.record({
44
68
  emit: (event) => {
69
+ if (!this._enabled)
70
+ return;
45
71
  this.events.push(event);
46
- // Auto-save when batch size reached
47
72
  if (this.events.length >= MAX_EVENT_BATCH) {
48
73
  this.save();
49
74
  }
@@ -56,8 +81,7 @@ export class MonoscopeReplay {
56
81
  tel: true,
57
82
  },
58
83
  maskTextClass: "rr-mask",
59
- // Performance settings
60
- checkoutEveryNms: 15 * 1000, // Full snapshot every 15s
84
+ checkoutEveryNms: 15 * 1000,
61
85
  sampling: {
62
86
  mouseInteraction: {
63
87
  MouseUp: false,
@@ -71,9 +95,9 @@ export class MonoscopeReplay {
71
95
  TouchEnd: false,
72
96
  },
73
97
  mousemove: true,
74
- scroll: 150, // Throttle scroll events
98
+ scroll: 150,
75
99
  media: 800,
76
- input: "last", // Only capture final input value
100
+ input: "last",
77
101
  },
78
102
  plugins: [
79
103
  getRecordConsolePlugin({
@@ -82,100 +106,81 @@ export class MonoscopeReplay {
82
106
  stringifyOptions: {
83
107
  stringLengthLimit: 1000,
84
108
  numOfKeysLimit: 100,
85
- depthOfLimit: 2, // Increased from 1 for better context
109
+ depthOfLimit: 2,
86
110
  },
87
111
  }),
88
112
  ],
89
113
  });
90
- this.saveInterval = setInterval(() => {
91
- this.save();
92
- }, SAVE_INTERVAL);
114
+ this.saveInterval = setInterval(() => this.save(), SAVE_INTERVAL);
93
115
  this.isConfigured = true;
94
- console.log("MonoscopeReplay configured successfully");
116
+ if (this.config.debug) {
117
+ console.log("MonoscopeReplay configured successfully");
118
+ }
95
119
  }
96
120
  catch (error) {
97
- console.error("Failed to configure MonoscopeReplay:", error);
98
- throw error;
121
+ console.warn("Monoscope: failed to configure replay", error);
99
122
  }
100
123
  }
101
124
  async save(forceSynchronous = false) {
102
- if (this.isSaving && !forceSynchronous) {
125
+ if (this.isSaving && !forceSynchronous)
103
126
  return;
104
- }
105
- if (this.events.length === 0) {
127
+ if (this.events.length === 0)
106
128
  return;
107
- }
108
- if (this.events.length > MAX_RETRY_EVENTS) {
109
- console.warn(`Event queue exceeded ${MAX_RETRY_EVENTS}, dropping middle events (preserving snapshots)`);
110
- // Find full snapshot events (type 2) - these are critical for replay
111
- const fullSnapshots = this.events.filter((e) => e.type === 2);
112
- const otherEvents = this.events.filter((e) => e.type !== 2);
113
- // Keep all snapshots and the most recent other events
114
- const remainingSlots = MAX_RETRY_EVENTS - fullSnapshots.length;
115
- this.events = [...fullSnapshots, ...otherEvents.slice(-remainingSlots)];
116
- // Re-sort by timestamp to maintain order
117
- this.events.sort((a, b) => a.timestamp - b.timestamp);
118
- }
129
+ this.trimEvents();
119
130
  this.isSaving = true;
120
- const { replayEventsBaseUrl, projectId } = this.config;
121
- // Construct base URL
122
- let baseUrl = replayEventsBaseUrl || "https://app.monoscope.tech";
123
- baseUrl = `${baseUrl}/rrweb/${projectId}`;
124
- // Get events to send and clear buffer
131
+ const baseUrl = `${this.config.replayEventsBaseUrl || "https://app.monoscope.tech"}/rrweb/${this.config.projectId}`;
125
132
  const eventsToSend = [...this.events];
126
133
  this.events = [];
127
134
  const payload = {
128
135
  events: eventsToSend,
129
136
  sessionId: this.sessionId,
137
+ tabId: this.tabId,
130
138
  timestamp: new Date().toISOString(),
131
139
  eventCount: eventsToSend.length,
140
+ user: Object.keys(this.userAttributes).length > 0 ? this.userAttributes : undefined,
132
141
  };
133
142
  try {
134
- if (forceSynchronous && navigator.sendBeacon) {
143
+ if (forceSynchronous && typeof navigator !== "undefined" && navigator.sendBeacon) {
135
144
  const blob = new Blob([JSON.stringify(payload)], {
136
145
  type: "application/json",
137
146
  });
138
147
  const sent = navigator.sendBeacon(baseUrl, blob);
139
148
  if (!sent) {
140
- console.warn("sendBeacon failed, events may be lost");
149
+ fetch(baseUrl, {
150
+ method: "POST",
151
+ headers: { "Content-Type": "application/json" },
152
+ body: JSON.stringify(payload),
153
+ keepalive: true,
154
+ }).catch(() => { });
141
155
  }
142
156
  }
143
157
  else {
144
- // Use keepalive so the request survives page navigation,
145
- // but only when payload fits under the 64KB keepalive limit
146
158
  const body = JSON.stringify(payload);
147
159
  const response = await fetch(baseUrl, {
148
160
  method: "POST",
149
- headers: {
150
- "Content-Type": "application/json",
151
- },
161
+ headers: { "Content-Type": "application/json" },
152
162
  body,
153
163
  keepalive: body.length < 63000,
154
164
  });
155
165
  if (!response.ok) {
156
166
  throw new Error(`Failed to save replay events: ${response.status} ${response.statusText}`);
157
167
  }
158
- console.log(`Successfully saved ${eventsToSend.length} replay events`);
168
+ if (this.config.debug) {
169
+ console.log(`Successfully saved ${eventsToSend.length} replay events`);
170
+ }
159
171
  }
160
172
  }
161
173
  catch (error) {
162
- console.error("Failed to save replay events:", error);
174
+ console.warn("Monoscope: failed to save replay events:", error);
163
175
  this.events = [...eventsToSend, ...this.events];
164
- if (this.events.length > MAX_RETRY_EVENTS) {
165
- // Preserve full snapshots when trimming
166
- const fullSnapshots = this.events.filter((e) => e.type === 2);
167
- const otherEvents = this.events.filter((e) => e.type !== 2);
168
- const remainingSlots = MAX_RETRY_EVENTS - fullSnapshots.length;
169
- this.events = [...fullSnapshots, ...otherEvents.slice(-remainingSlots)];
170
- this.events.sort((a, b) => a.timestamp - b.timestamp);
171
- }
176
+ this.trimEvents();
172
177
  }
173
178
  finally {
174
179
  this.isSaving = false;
175
180
  }
176
181
  }
177
182
  stop() {
178
- this.save(true);
183
+ this.save(true).catch(() => { });
179
184
  if (this.stopRecording) {
180
185
  this.stopRecording();
181
186
  this.stopRecording = undefined;
@@ -184,19 +189,30 @@ export class MonoscopeReplay {
184
189
  clearInterval(this.saveInterval);
185
190
  this.saveInterval = null;
186
191
  }
187
- window.removeEventListener("beforeunload", this.handleUnload);
188
- window.removeEventListener("pagehide", this.handleUnload);
189
- document.removeEventListener("visibilitychange", this.handleVisibilityChange);
192
+ this.removeListeners();
190
193
  this.isConfigured = false;
191
- console.log("MonoscopeReplay stopped");
194
+ if (this.config.debug) {
195
+ console.log("MonoscopeReplay stopped");
196
+ }
197
+ }
198
+ setEnabled(enabled) {
199
+ this._enabled = enabled;
200
+ if (!enabled)
201
+ this.stop();
202
+ }
203
+ setUser(user) {
204
+ this.userAttributes = { ...this.userAttributes, ...user };
192
205
  }
193
206
  getEventCount() {
194
207
  return this.events.length;
195
208
  }
209
+ updateSessionId(sessionId) {
210
+ this.sessionId = sessionId;
211
+ }
196
212
  getSessionId() {
197
213
  return this.sessionId;
198
214
  }
199
215
  isRecording() {
200
- return this.isConfigured && this.stopRecording !== null;
216
+ return this.isConfigured && this.stopRecording !== undefined;
201
217
  }
202
218
  }
@@ -0,0 +1,13 @@
1
+ type EmitFn = (name: string, attrs: Record<string, string | number>) => void;
2
+ export declare class SPARouter {
3
+ private emit;
4
+ private currentUrl;
5
+ private _active;
6
+ private origPushState;
7
+ private origReplaceState;
8
+ private popstateHandler;
9
+ constructor(emit: EmitFn);
10
+ start(): void;
11
+ stop(): void;
12
+ }
13
+ export {};
package/dist/router.js ADDED
@@ -0,0 +1,65 @@
1
+ import { addBreadcrumb } from "./breadcrumbs";
2
+ export class SPARouter {
3
+ constructor(emit) {
4
+ this.currentUrl = "";
5
+ this._active = false;
6
+ this.origPushState = null;
7
+ this.origReplaceState = null;
8
+ this.popstateHandler = null;
9
+ this.emit = emit;
10
+ }
11
+ start() {
12
+ if (typeof window === "undefined" || this._active)
13
+ return;
14
+ this._active = true;
15
+ this.currentUrl = location.href;
16
+ this.origPushState = history.pushState.bind(history);
17
+ this.origReplaceState = history.replaceState.bind(history);
18
+ const onNav = (method) => {
19
+ try {
20
+ const from = this.currentUrl;
21
+ const to = location.href;
22
+ if (from === to)
23
+ return;
24
+ this.currentUrl = to;
25
+ addBreadcrumb({ type: "navigation", message: `${from} → ${to}`, data: { method } });
26
+ this.emit("navigation", {
27
+ "navigation.from": from,
28
+ "navigation.to": to,
29
+ "navigation.method": method,
30
+ "page.title": document.title,
31
+ });
32
+ }
33
+ catch (e) {
34
+ try {
35
+ console.warn("Monoscope: error in navigation tracking", e);
36
+ }
37
+ catch { /* must never throw */ }
38
+ }
39
+ };
40
+ history.pushState = (...args) => {
41
+ this.origPushState(...args);
42
+ onNav("pushState");
43
+ };
44
+ history.replaceState = (...args) => {
45
+ this.origReplaceState(...args);
46
+ onNav("replaceState");
47
+ };
48
+ this.popstateHandler = () => onNav("popstate");
49
+ window.addEventListener("popstate", this.popstateHandler);
50
+ }
51
+ stop() {
52
+ if (typeof window === "undefined" || !this._active)
53
+ return;
54
+ this._active = false;
55
+ if (this.origPushState)
56
+ history.pushState = this.origPushState;
57
+ if (this.origReplaceState)
58
+ history.replaceState = this.origReplaceState;
59
+ if (this.popstateHandler)
60
+ window.removeEventListener("popstate", this.popstateHandler);
61
+ this.origPushState = null;
62
+ this.origReplaceState = null;
63
+ this.popstateHandler = null;
64
+ }
65
+ }
package/dist/tracing.d.ts CHANGED
@@ -1,12 +1,32 @@
1
1
  import { MonoscopeConfig, MonoscopeUser } from "./types";
2
+ import { Span } from "@opentelemetry/api";
2
3
  export declare class OpenTelemetryManager {
3
4
  private config;
4
5
  private sessionId;
6
+ private tabId;
5
7
  private provider;
6
- constructor(config: MonoscopeConfig, sessionId: string);
8
+ private longTaskObserver;
9
+ private resourceObserver;
10
+ private _enabled;
11
+ private _configured;
12
+ private pageSpan;
13
+ private pageContext;
14
+ private endPageSpanHandler;
15
+ constructor(config: MonoscopeConfig, sessionId: string, tabId: string);
7
16
  private createProvider;
17
+ private commonAttrs;
18
+ private applyCommonAttrs;
8
19
  configure(): void;
9
- private setUserAttributes;
20
+ private startPageSpan;
21
+ getPageContext(): import("@opentelemetry/api").Context | null;
22
+ private withPageContext;
23
+ emitSpan(name: string, attrs: Record<string, string | number | boolean>, configure?: (span: Span) => void): void;
24
+ private observeLongTasks;
25
+ private observeResourceTiming;
26
+ startSpan<T>(name: string, fn: (span: Span) => T): T;
27
+ recordEvent(name: string, attributes?: Record<string, string | number | boolean>): void;
28
+ updateSessionId(sessionId: string): void;
29
+ setEnabled(enabled: boolean): void;
10
30
  shutdown(): Promise<void>;
11
31
  setUser(newConfig: MonoscopeUser): void;
12
32
  }