@monoscopetech/browser 0.6.1 → 0.7.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/README.md +215 -27
- package/dist/breadcrumbs.d.ts +9 -0
- package/dist/breadcrumbs.js +13 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +107 -0
- package/dist/index.d.ts +20 -1
- package/dist/index.js +161 -14
- package/dist/monoscope.min.js +5 -5
- package/dist/monoscope.min.js.map +1 -1
- package/dist/monoscope.umd.js +5 -5
- package/dist/monoscope.umd.js.map +1 -1
- package/dist/react.d.ts +28 -0
- package/dist/react.js +59 -0
- package/dist/replay.d.ts +11 -2
- package/dist/replay.js +80 -72
- package/dist/router.d.ts +13 -0
- package/dist/router.js +65 -0
- package/dist/tracing.d.ts +22 -2
- package/dist/tracing.js +196 -43
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +7 -2
- package/dist/web-vitals.d.ts +10 -0
- package/dist/web-vitals.js +31 -0
- package/package.json +40 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Monoscope Browser SDK
|
|
2
2
|
|
|
3
|
-
The **Monoscope Browser SDK** is a lightweight JavaScript library for adding **session replay**, **performance tracing**, and **
|
|
3
|
+
The **Monoscope Browser SDK** is a lightweight JavaScript library for adding **session replay**, **performance tracing**, **error tracking**, and **web vitals** to your web applications.
|
|
4
4
|
|
|
5
5
|
When used together with the [Monoscope Server SDKs](https://apitoolkit.io/docs/sdks/), you gain **end-to-end observability** — seamlessly connecting user interactions in the browser to backend services, APIs, and databases.
|
|
6
6
|
|
|
@@ -8,9 +8,9 @@ This means you can:
|
|
|
8
8
|
|
|
9
9
|
- **Replay user sessions** to see exactly what happened.
|
|
10
10
|
- **Trace requests** from the frontend, through your backend, and into your database.
|
|
11
|
-
- **Capture
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
- **Capture errors and console logs** with full context and breadcrumbs for faster debugging.
|
|
12
|
+
- **Collect Web Vitals** (CLS, INP, LCP, FCP, TTFB) automatically.
|
|
13
|
+
- **Track SPA navigations** across pushState, replaceState, and popstate.
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
@@ -40,7 +40,12 @@ import Monoscope from "@monoscopetech/browser";
|
|
|
40
40
|
const monoscope = new Monoscope({
|
|
41
41
|
projectId: "YOUR_PROJECT_ID",
|
|
42
42
|
serviceName: "my-web-app",
|
|
43
|
-
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Identify the current user
|
|
46
|
+
monoscope.setUser({
|
|
47
|
+
id: "user-123",
|
|
48
|
+
email: "user@example.com",
|
|
44
49
|
});
|
|
45
50
|
```
|
|
46
51
|
|
|
@@ -50,16 +55,23 @@ const monoscope = new Monoscope({
|
|
|
50
55
|
|
|
51
56
|
The `Monoscope` constructor accepts the following options:
|
|
52
57
|
|
|
53
|
-
| Name
|
|
54
|
-
|
|
|
55
|
-
| `projectId`
|
|
56
|
-
| `serviceName`
|
|
57
|
-
| `exporterEndpoint`
|
|
58
|
-
| `propagateTraceHeaderCorsUrls` | `RegExp[]`
|
|
59
|
-
| `resourceAttributes`
|
|
60
|
-
| `instrumentations`
|
|
61
|
-
| `replayEventsBaseUrl`
|
|
62
|
-
| `
|
|
58
|
+
| Name | Type | Description |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| `projectId` | `string` | **Required** – Your Monoscope project ID. |
|
|
61
|
+
| `serviceName` | `string` | **Required** – Name of your service or application. |
|
|
62
|
+
| `exporterEndpoint` | `string` | Endpoint for exporting traces. Defaults to Monoscope's ingest endpoint. |
|
|
63
|
+
| `propagateTraceHeaderCorsUrls` | `RegExp[]` | URL patterns where trace context headers should be propagated. Defaults to same-origin only. |
|
|
64
|
+
| `resourceAttributes` | `Record<string, string>` | Additional OpenTelemetry resource attributes. |
|
|
65
|
+
| `instrumentations` | `unknown[]` | Additional OpenTelemetry instrumentations to register. |
|
|
66
|
+
| `replayEventsBaseUrl` | `string` | Base URL for session replay events. Defaults to Monoscope's ingest endpoint. |
|
|
67
|
+
| `enableNetworkEvents` | `boolean` | Include network timing events in document load spans. |
|
|
68
|
+
| `user` | `MonoscopeUser` | Default user information for the session. |
|
|
69
|
+
| `debug` | `boolean` | Enable debug logging to the console. |
|
|
70
|
+
| `sampleRate` | `number` | Trace sampling rate from `0` to `1`. Default `1` (100%). |
|
|
71
|
+
| `replaySampleRate` | `number` | Replay sampling rate from `0` to `1`. Default `1` (100%). |
|
|
72
|
+
| `enabled` | `boolean` | Whether to start collecting data immediately. Default `true`. |
|
|
73
|
+
| `resourceTimingThresholdMs` | `number` | Minimum resource duration (ms) to report. Default `200`. |
|
|
74
|
+
| `enableUserInteraction` | `boolean` | Trace user clicks and interactions, linking them to downstream network calls. Default `false`. |
|
|
63
75
|
|
|
64
76
|
---
|
|
65
77
|
|
|
@@ -67,13 +79,15 @@ The `Monoscope` constructor accepts the following options:
|
|
|
67
79
|
|
|
68
80
|
The `MonoscopeUser` object can contain:
|
|
69
81
|
|
|
70
|
-
| Name
|
|
71
|
-
|
|
|
72
|
-
| `email`
|
|
73
|
-
| `
|
|
74
|
-
| `name`
|
|
75
|
-
| `id`
|
|
76
|
-
| `roles`
|
|
82
|
+
| Name | Type | Description |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| `email` | `string` | User's email address. |
|
|
85
|
+
| `full_name` | `string` | User's full name. |
|
|
86
|
+
| `name` | `string` | User's preferred name. |
|
|
87
|
+
| `id` | `string` | User's unique identifier. |
|
|
88
|
+
| `roles` | `string[]` | User's roles. |
|
|
89
|
+
|
|
90
|
+
Additional string-keyed attributes are also accepted and forwarded as custom user attributes.
|
|
77
91
|
|
|
78
92
|
---
|
|
79
93
|
|
|
@@ -81,7 +95,7 @@ The `MonoscopeUser` object can contain:
|
|
|
81
95
|
|
|
82
96
|
### `setUser(user: MonoscopeUser)`
|
|
83
97
|
|
|
84
|
-
Associates the given user with the current session.
|
|
98
|
+
Associates the given user with the current session. Can be called at any time.
|
|
85
99
|
|
|
86
100
|
```javascript
|
|
87
101
|
monoscope.setUser({
|
|
@@ -90,17 +104,191 @@ monoscope.setUser({
|
|
|
90
104
|
});
|
|
91
105
|
```
|
|
92
106
|
|
|
93
|
-
|
|
107
|
+
### `startSpan<T>(name: string, fn: (span: Span) => T): T`
|
|
108
|
+
|
|
109
|
+
Creates a custom OpenTelemetry span. The span is automatically ended when the function returns. Supports async functions.
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
monoscope.startSpan("checkout", (span) => {
|
|
113
|
+
span.setAttribute("cart.items", 3);
|
|
114
|
+
// ... your logic
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### `recordEvent(name: string, attributes?: Record<string, string | number | boolean>)`
|
|
119
|
+
|
|
120
|
+
Records a custom event as a span with the given attributes.
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
monoscope.recordEvent("button_click", {
|
|
124
|
+
"button.name": "subscribe",
|
|
125
|
+
"button.variant": "primary",
|
|
126
|
+
});
|
|
127
|
+
```
|
|
94
128
|
|
|
95
129
|
### `getSessionId(): string`
|
|
96
130
|
|
|
97
|
-
|
|
131
|
+
Returns the current session ID.
|
|
132
|
+
|
|
133
|
+
### `getTabId(): string`
|
|
134
|
+
|
|
135
|
+
Returns the current tab ID (unique per browser tab).
|
|
136
|
+
|
|
137
|
+
### `enable()` / `disable()`
|
|
138
|
+
|
|
139
|
+
Dynamically enable or disable all data collection.
|
|
98
140
|
|
|
99
141
|
```javascript
|
|
100
|
-
|
|
101
|
-
|
|
142
|
+
monoscope.disable(); // pause collection
|
|
143
|
+
monoscope.enable(); // resume collection
|
|
102
144
|
```
|
|
103
145
|
|
|
146
|
+
### `isEnabled(): boolean`
|
|
147
|
+
|
|
148
|
+
Returns whether the SDK is currently enabled.
|
|
149
|
+
|
|
150
|
+
### `destroy(): Promise<void>`
|
|
151
|
+
|
|
152
|
+
Stops all collection, flushes pending data, and shuts down the OpenTelemetry provider. Call this when your application is being torn down.
|
|
153
|
+
|
|
154
|
+
```javascript
|
|
155
|
+
await monoscope.destroy();
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## React / Next.js
|
|
161
|
+
|
|
162
|
+
For React apps, use the `@monoscopetech/browser/react` subpath export for idiomatic integration with hooks and context.
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
import { MonoscopeProvider, useMonoscope, useMonoscopeUser, MonoscopeErrorBoundary } from "@monoscopetech/browser/react";
|
|
166
|
+
|
|
167
|
+
// Wrap your app with MonoscopeProvider
|
|
168
|
+
function App() {
|
|
169
|
+
return (
|
|
170
|
+
<MonoscopeProvider config={{ projectId: "YOUR_PROJECT_ID", serviceName: "my-react-app" }}>
|
|
171
|
+
<MonoscopeErrorBoundary fallback={<div>Something went wrong</div>}>
|
|
172
|
+
<MyApp />
|
|
173
|
+
</MonoscopeErrorBoundary>
|
|
174
|
+
</MonoscopeProvider>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Access the instance via hook
|
|
179
|
+
function MyApp() {
|
|
180
|
+
const monoscope = useMonoscope();
|
|
181
|
+
|
|
182
|
+
// Reactively set user when auth state changes
|
|
183
|
+
useMonoscopeUser(currentUser ? { id: currentUser.id, email: currentUser.email } : null);
|
|
184
|
+
|
|
185
|
+
return <div>...</div>;
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Next.js App Router**: The provider includes `"use client"` — import it in a client component or your root layout.
|
|
190
|
+
|
|
191
|
+
### React API
|
|
192
|
+
|
|
193
|
+
| Export | Description |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| `MonoscopeProvider` | Context provider. Creates and destroys the SDK instance. Strict Mode safe. |
|
|
196
|
+
| `useMonoscope()` | Returns the `Monoscope` instance (or `null` during SSR). |
|
|
197
|
+
| `useMonoscopeUser(user)` | Calls `setUser` reactively when the user object changes. |
|
|
198
|
+
| `MonoscopeErrorBoundary` | Error boundary that reports caught errors to Monoscope. Accepts `fallback` prop. |
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Custom Instrumentation
|
|
203
|
+
|
|
204
|
+
### Custom Spans
|
|
205
|
+
|
|
206
|
+
Use `startSpan()` to instrument specific operations with timing and attributes. It supports both sync and async functions — the span is automatically ended when the function returns or the promise resolves.
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
// Sync
|
|
210
|
+
monoscope.startSpan("parse-config", (span) => {
|
|
211
|
+
span.setAttribute("config.size", rawConfig.length);
|
|
212
|
+
return parseConfig(rawConfig);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Async
|
|
216
|
+
const data = await monoscope.startSpan("fetch-dashboard", async (span) => {
|
|
217
|
+
span.setAttribute("dashboard.id", dashboardId);
|
|
218
|
+
const res = await fetch(`/api/dashboards/${dashboardId}`);
|
|
219
|
+
span.setAttribute("http.status", res.status);
|
|
220
|
+
return res.json();
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Custom Events
|
|
225
|
+
|
|
226
|
+
Use `recordEvent()` to track discrete events without wrapping a code block:
|
|
227
|
+
|
|
228
|
+
```javascript
|
|
229
|
+
monoscope.recordEvent("feature_flag_evaluated", {
|
|
230
|
+
"flag.name": "new-checkout",
|
|
231
|
+
"flag.value": true,
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### React Components
|
|
236
|
+
|
|
237
|
+
Use the `useMonoscope()` hook to instrument React components:
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
import { useMonoscope } from "@monoscopetech/browser/react";
|
|
241
|
+
|
|
242
|
+
function CheckoutButton() {
|
|
243
|
+
const monoscope = useMonoscope();
|
|
244
|
+
|
|
245
|
+
const handleClick = () => {
|
|
246
|
+
monoscope?.startSpan("checkout.submit", async (span) => {
|
|
247
|
+
span.setAttribute("cart.items", cartItems.length);
|
|
248
|
+
await submitOrder();
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
return <button onClick={handleClick}>Checkout</button>;
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Additional OpenTelemetry Instrumentations
|
|
257
|
+
|
|
258
|
+
Pass extra OTel instrumentations via the `instrumentations` config to extend tracing beyond the built-in set:
|
|
259
|
+
|
|
260
|
+
```javascript
|
|
261
|
+
import { LongTaskInstrumentation } from "@opentelemetry/instrumentation-long-task";
|
|
262
|
+
|
|
263
|
+
const monoscope = new Monoscope({
|
|
264
|
+
projectId: "YOUR_PROJECT_ID",
|
|
265
|
+
serviceName: "my-app",
|
|
266
|
+
instrumentations: [new LongTaskInstrumentation()],
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Features
|
|
273
|
+
|
|
274
|
+
### Session Replay
|
|
275
|
+
Captures DOM changes via [rrweb](https://github.com/rrweb-io/rrweb) to enable full session replay. Sensitive inputs are masked by default.
|
|
276
|
+
|
|
277
|
+
### Error Tracking
|
|
278
|
+
Automatically captures `window.onerror`, unhandled promise rejections, and `console.error` calls with stack traces and breadcrumbs.
|
|
279
|
+
|
|
280
|
+
### SPA Navigation Tracking
|
|
281
|
+
Detects client-side navigations (`pushState`, `replaceState`, `popstate`) and emits navigation spans.
|
|
282
|
+
|
|
283
|
+
### Web Vitals
|
|
284
|
+
Collects Core Web Vitals (CLS, INP, LCP) and additional metrics (FCP, TTFB) via the [web-vitals](https://github.com/GoogleChrome/web-vitals) library.
|
|
285
|
+
|
|
286
|
+
### Performance Observers
|
|
287
|
+
Reports long tasks and slow resource loads as spans for performance debugging.
|
|
288
|
+
|
|
289
|
+
### Session Management
|
|
290
|
+
Sessions persist across page reloads via `sessionStorage` and automatically rotate after 30 minutes of inactivity.
|
|
291
|
+
|
|
104
292
|
---
|
|
105
293
|
|
|
106
294
|
## License
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type Breadcrumb = {
|
|
2
|
+
type: "click" | "navigation" | "console.error" | "http" | "custom";
|
|
3
|
+
message: string;
|
|
4
|
+
timestamp: number;
|
|
5
|
+
data?: Record<string, string>;
|
|
6
|
+
};
|
|
7
|
+
export declare function addBreadcrumb(crumb: Omit<Breadcrumb, "timestamp">): void;
|
|
8
|
+
export declare function getBreadcrumbs(): Breadcrumb[];
|
|
9
|
+
export declare function clearBreadcrumbs(): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const MAX_BREADCRUMBS = 20;
|
|
2
|
+
const buffer = [];
|
|
3
|
+
export function addBreadcrumb(crumb) {
|
|
4
|
+
buffer.push({ ...crumb, timestamp: Date.now() });
|
|
5
|
+
if (buffer.length > MAX_BREADCRUMBS)
|
|
6
|
+
buffer.shift();
|
|
7
|
+
}
|
|
8
|
+
export function getBreadcrumbs() {
|
|
9
|
+
return buffer.slice();
|
|
10
|
+
}
|
|
11
|
+
export function clearBreadcrumbs() {
|
|
12
|
+
buffer.length = 0;
|
|
13
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Span } from "@opentelemetry/api";
|
|
2
|
+
type EmitFn = (name: string, attrs: Record<string, string | number>, configure?: (span: Span) => void) => void;
|
|
3
|
+
export declare class ErrorTracker {
|
|
4
|
+
private emit;
|
|
5
|
+
private errorCount;
|
|
6
|
+
private _active;
|
|
7
|
+
private prevOnError;
|
|
8
|
+
private onUnhandledRejection;
|
|
9
|
+
private origConsoleError;
|
|
10
|
+
private _processing;
|
|
11
|
+
constructor(emit: EmitFn);
|
|
12
|
+
private createErrorSpan;
|
|
13
|
+
start(): void;
|
|
14
|
+
stop(): void;
|
|
15
|
+
getErrorCount(): number;
|
|
16
|
+
}
|
|
17
|
+
export {};
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { SpanStatusCode } from "@opentelemetry/api";
|
|
2
|
+
import { addBreadcrumb, getBreadcrumbs } from "./breadcrumbs";
|
|
3
|
+
function safeStringify(val) {
|
|
4
|
+
try {
|
|
5
|
+
return JSON.stringify(val);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return `[unserializable: ${typeof val}]`;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class ErrorTracker {
|
|
12
|
+
constructor(emit) {
|
|
13
|
+
this.errorCount = 0;
|
|
14
|
+
this._active = false;
|
|
15
|
+
this.prevOnError = null;
|
|
16
|
+
this.onUnhandledRejection = null;
|
|
17
|
+
this.origConsoleError = null;
|
|
18
|
+
this._processing = false;
|
|
19
|
+
this.emit = emit;
|
|
20
|
+
}
|
|
21
|
+
createErrorSpan(spanName, errorType, attrs) {
|
|
22
|
+
this.errorCount++;
|
|
23
|
+
const crumbs = getBreadcrumbs();
|
|
24
|
+
this.emit(spanName, { "error.type": errorType, "error.count": this.errorCount, ...attrs }, (s) => {
|
|
25
|
+
s.setStatus({ code: SpanStatusCode.ERROR });
|
|
26
|
+
if (crumbs.length > 0)
|
|
27
|
+
s.setAttribute("breadcrumbs", safeStringify(crumbs));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
start() {
|
|
31
|
+
if (typeof window === "undefined" || this._active)
|
|
32
|
+
return;
|
|
33
|
+
this._active = true;
|
|
34
|
+
this.prevOnError = window.onerror;
|
|
35
|
+
window.onerror = (event, source, lineno, colno, error) => {
|
|
36
|
+
const attrs = {
|
|
37
|
+
"error.message": typeof event === "string" ? event : event.type,
|
|
38
|
+
};
|
|
39
|
+
if (source)
|
|
40
|
+
attrs["error.source"] = source;
|
|
41
|
+
if (lineno !== undefined)
|
|
42
|
+
attrs["error.lineno"] = lineno;
|
|
43
|
+
if (colno !== undefined)
|
|
44
|
+
attrs["error.colno"] = colno;
|
|
45
|
+
if (error?.stack)
|
|
46
|
+
attrs["error.stack"] = error.stack;
|
|
47
|
+
if (error?.name)
|
|
48
|
+
attrs["error.name"] = error.name;
|
|
49
|
+
this.createErrorSpan("exception", "uncaught_exception", attrs);
|
|
50
|
+
if (typeof this.prevOnError === "function") {
|
|
51
|
+
return this.prevOnError.call(window, event, source, lineno, colno, error);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
this.onUnhandledRejection = (event) => {
|
|
55
|
+
const reason = event.reason;
|
|
56
|
+
const attrs = {};
|
|
57
|
+
if (reason instanceof Error) {
|
|
58
|
+
attrs["error.message"] = reason.message;
|
|
59
|
+
attrs["error.name"] = reason.name;
|
|
60
|
+
if (reason.stack)
|
|
61
|
+
attrs["error.stack"] = reason.stack;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
attrs["error.message"] = String(reason);
|
|
65
|
+
}
|
|
66
|
+
this.createErrorSpan("unhandled_rejection", "unhandled_rejection", attrs);
|
|
67
|
+
};
|
|
68
|
+
this.origConsoleError = console.error;
|
|
69
|
+
console.error = (...args) => {
|
|
70
|
+
this.origConsoleError?.apply(console, args);
|
|
71
|
+
if (this._processing)
|
|
72
|
+
return;
|
|
73
|
+
this._processing = true;
|
|
74
|
+
try {
|
|
75
|
+
const message = args.map((a) => a instanceof Error ? a.message : typeof a === "string" ? a : safeStringify(a)).join(" ");
|
|
76
|
+
const attrs = { "error.message": message };
|
|
77
|
+
const errorArg = args.find((a) => a instanceof Error);
|
|
78
|
+
if (errorArg) {
|
|
79
|
+
attrs["error.name"] = errorArg.name;
|
|
80
|
+
if (errorArg.stack)
|
|
81
|
+
attrs["error.stack"] = errorArg.stack;
|
|
82
|
+
}
|
|
83
|
+
addBreadcrumb({ type: "console.error", message });
|
|
84
|
+
this.createErrorSpan("console.error", "console_error", attrs);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
this._processing = false;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
window.addEventListener("unhandledrejection", this.onUnhandledRejection);
|
|
91
|
+
}
|
|
92
|
+
stop() {
|
|
93
|
+
if (typeof window === "undefined" || !this._active)
|
|
94
|
+
return;
|
|
95
|
+
this._active = false;
|
|
96
|
+
window.onerror = this.prevOnError;
|
|
97
|
+
this.prevOnError = null;
|
|
98
|
+
if (this.onUnhandledRejection) {
|
|
99
|
+
window.removeEventListener("unhandledrejection", this.onUnhandledRejection);
|
|
100
|
+
}
|
|
101
|
+
if (this.origConsoleError) {
|
|
102
|
+
console.error = this.origConsoleError;
|
|
103
|
+
this.origConsoleError = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
getErrorCount() { return this.errorCount; }
|
|
107
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
import { MonoscopeReplay } from "./replay";
|
|
2
2
|
import { OpenTelemetryManager } from "./tracing";
|
|
3
3
|
import { MonoscopeConfig, MonoscopeUser } from "./types";
|
|
4
|
+
import type { Span } from "@opentelemetry/api";
|
|
4
5
|
declare class Monoscope {
|
|
5
6
|
replay: MonoscopeReplay;
|
|
6
|
-
config
|
|
7
|
+
private config;
|
|
7
8
|
otel: OpenTelemetryManager;
|
|
8
9
|
sessionId: string;
|
|
10
|
+
tabId: string;
|
|
11
|
+
private lastActivityTime;
|
|
12
|
+
private errors;
|
|
13
|
+
private vitals;
|
|
14
|
+
private router;
|
|
15
|
+
private _enabled;
|
|
9
16
|
constructor(config: MonoscopeConfig);
|
|
17
|
+
private resolveSessionId;
|
|
18
|
+
private persistActivity;
|
|
19
|
+
private rotateSession;
|
|
20
|
+
private checkAndRefreshSession;
|
|
21
|
+
private setupActivityTracking;
|
|
10
22
|
getSessionId(): string;
|
|
23
|
+
getTabId(): string;
|
|
11
24
|
setUser(u: MonoscopeUser): void;
|
|
25
|
+
startSpan<T>(name: string, fn: (span: Span) => T): T;
|
|
26
|
+
recordEvent(name: string, attributes?: Record<string, string | number | boolean>): void;
|
|
27
|
+
disable(): void;
|
|
28
|
+
enable(): void;
|
|
29
|
+
isEnabled(): boolean;
|
|
30
|
+
destroy(): Promise<void>;
|
|
12
31
|
}
|
|
13
32
|
export default Monoscope;
|
package/dist/index.js
CHANGED
|
@@ -1,30 +1,177 @@
|
|
|
1
1
|
import { MonoscopeReplay } from "./replay";
|
|
2
2
|
import { OpenTelemetryManager } from "./tracing";
|
|
3
|
-
import {
|
|
3
|
+
import { ErrorTracker } from "./errors";
|
|
4
|
+
import { WebVitalsCollector } from "./web-vitals";
|
|
5
|
+
import { SPARouter } from "./router";
|
|
6
|
+
import { addBreadcrumb, clearBreadcrumbs } from "./breadcrumbs";
|
|
7
|
+
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
8
|
+
const LAST_ACTIVITY_KEY = "monoscope-last-activity";
|
|
9
|
+
const isBrowser = typeof window !== "undefined";
|
|
4
10
|
class Monoscope {
|
|
5
11
|
constructor(config) {
|
|
6
|
-
if (!config.projectId)
|
|
12
|
+
if (!config.projectId)
|
|
7
13
|
throw new Error("MonoscopeConfig must include projectId");
|
|
14
|
+
if (!config.serviceName)
|
|
15
|
+
throw new Error("MonoscopeConfig must include serviceName");
|
|
16
|
+
this.config = config;
|
|
17
|
+
this._enabled = config.enabled !== false;
|
|
18
|
+
this.tabId = crypto.randomUUID();
|
|
19
|
+
this.sessionId = isBrowser ? this.resolveSessionId() : crypto.randomUUID();
|
|
20
|
+
this.lastActivityTime = Date.now();
|
|
21
|
+
if (isBrowser)
|
|
22
|
+
this.persistActivity();
|
|
23
|
+
this.replay = new MonoscopeReplay(config, this.sessionId, this.tabId);
|
|
24
|
+
this.otel = new OpenTelemetryManager(config, this.sessionId, this.tabId);
|
|
25
|
+
const emit = (...args) => this.otel.emitSpan(...args);
|
|
26
|
+
this.errors = new ErrorTracker(emit);
|
|
27
|
+
this.vitals = new WebVitalsCollector(emit);
|
|
28
|
+
this.router = new SPARouter(emit);
|
|
29
|
+
if (this._enabled) {
|
|
30
|
+
this.otel.configure();
|
|
31
|
+
this.replay.configure();
|
|
32
|
+
this.errors.start();
|
|
33
|
+
this.vitals.start().catch((e) => {
|
|
34
|
+
if (this.config.debug)
|
|
35
|
+
console.warn("Monoscope: web-vitals init failed", e);
|
|
36
|
+
});
|
|
37
|
+
this.router.start();
|
|
38
|
+
}
|
|
39
|
+
if (isBrowser)
|
|
40
|
+
this.setupActivityTracking();
|
|
41
|
+
}
|
|
42
|
+
resolveSessionId() {
|
|
43
|
+
try {
|
|
44
|
+
const storedId = sessionStorage.getItem("monoscope-session-id");
|
|
45
|
+
const lastActivity = sessionStorage.getItem(LAST_ACTIVITY_KEY);
|
|
46
|
+
if (storedId && lastActivity) {
|
|
47
|
+
const elapsed = Date.now() - parseInt(lastActivity, 10);
|
|
48
|
+
if (elapsed < SESSION_TIMEOUT_MS)
|
|
49
|
+
return storedId;
|
|
50
|
+
}
|
|
51
|
+
const newId = crypto.randomUUID();
|
|
52
|
+
sessionStorage.setItem("monoscope-session-id", newId);
|
|
53
|
+
return newId;
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
if (this.config.debug)
|
|
57
|
+
console.warn("Monoscope: sessionStorage unavailable, using ephemeral session", e);
|
|
58
|
+
return crypto.randomUUID();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
persistActivity() {
|
|
62
|
+
try {
|
|
63
|
+
sessionStorage.setItem(LAST_ACTIVITY_KEY, Date.now().toString());
|
|
8
64
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
65
|
+
catch (e) {
|
|
66
|
+
if (this.config.debug)
|
|
67
|
+
console.warn("Monoscope: failed to persist activity", e);
|
|
12
68
|
}
|
|
13
|
-
|
|
14
|
-
|
|
69
|
+
}
|
|
70
|
+
rotateSession() {
|
|
71
|
+
this.replay.save().catch((e) => {
|
|
72
|
+
if (this.config.debug)
|
|
73
|
+
console.warn("Monoscope: failed to save replay on session rotation", e);
|
|
74
|
+
});
|
|
75
|
+
clearBreadcrumbs();
|
|
76
|
+
this.sessionId = crypto.randomUUID();
|
|
77
|
+
try {
|
|
15
78
|
sessionStorage.setItem("monoscope-session-id", this.sessionId);
|
|
16
79
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.replay.
|
|
80
|
+
catch (e) {
|
|
81
|
+
if (this.config.debug)
|
|
82
|
+
console.warn("Monoscope: failed to persist session ID", e);
|
|
83
|
+
}
|
|
84
|
+
this.replay.updateSessionId(this.sessionId);
|
|
85
|
+
this.otel.updateSessionId(this.sessionId);
|
|
86
|
+
if (this.config.debug)
|
|
87
|
+
console.log("Monoscope: session rotated due to inactivity");
|
|
22
88
|
}
|
|
23
|
-
|
|
24
|
-
|
|
89
|
+
checkAndRefreshSession() {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
if (now - this.lastActivityTime >= SESSION_TIMEOUT_MS) {
|
|
92
|
+
this.rotateSession();
|
|
93
|
+
}
|
|
94
|
+
this.lastActivityTime = now;
|
|
95
|
+
this.persistActivity();
|
|
25
96
|
}
|
|
97
|
+
setupActivityTracking() {
|
|
98
|
+
document.addEventListener("visibilitychange", () => {
|
|
99
|
+
if (document.visibilityState === "visible") {
|
|
100
|
+
this.checkAndRefreshSession();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
let lastTracked = Date.now();
|
|
104
|
+
const trackActivity = () => {
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
if (now - lastTracked > 5000) {
|
|
107
|
+
lastTracked = now;
|
|
108
|
+
this.lastActivityTime = now;
|
|
109
|
+
this.persistActivity();
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
document.addEventListener("click", (e) => {
|
|
113
|
+
trackActivity();
|
|
114
|
+
const el = e.target;
|
|
115
|
+
if (!el)
|
|
116
|
+
return;
|
|
117
|
+
const tag = el.tagName?.toLowerCase() || "";
|
|
118
|
+
const text = (el.textContent || "").trim().slice(0, 50);
|
|
119
|
+
const cls = el.getAttribute("class") || "";
|
|
120
|
+
const selector = el.id ? `#${el.id}` : cls ? `.${cls.split(" ")[0]}` : tag;
|
|
121
|
+
addBreadcrumb({ type: "click", message: `${tag} "${text}"`, data: { selector } });
|
|
122
|
+
}, { capture: true, passive: true });
|
|
123
|
+
document.addEventListener("keydown", trackActivity, { capture: true, passive: true });
|
|
124
|
+
}
|
|
125
|
+
getSessionId() { return this.sessionId; }
|
|
126
|
+
getTabId() { return this.tabId; }
|
|
26
127
|
setUser(u) {
|
|
128
|
+
if (this.config.debug) {
|
|
129
|
+
const known = new Set(["email", "full_name", "name", "id", "roles"]);
|
|
130
|
+
const extra = Object.keys(u).filter(k => !known.has(k));
|
|
131
|
+
if (extra.length)
|
|
132
|
+
console.warn(`Monoscope: unknown user attributes will be sent to collectors: ${extra.join(", ")}`);
|
|
133
|
+
}
|
|
27
134
|
this.otel.setUser(u);
|
|
135
|
+
this.replay.setUser(u);
|
|
136
|
+
}
|
|
137
|
+
startSpan(name, fn) {
|
|
138
|
+
return this.otel.startSpan(name, fn);
|
|
139
|
+
}
|
|
140
|
+
recordEvent(name, attributes) {
|
|
141
|
+
this.otel.recordEvent(name, attributes);
|
|
142
|
+
}
|
|
143
|
+
disable() {
|
|
144
|
+
this._enabled = false;
|
|
145
|
+
this.replay.setEnabled(false);
|
|
146
|
+
this.otel.setEnabled(false);
|
|
147
|
+
this.vitals.setEnabled(false);
|
|
148
|
+
this.errors.stop();
|
|
149
|
+
this.router.stop();
|
|
150
|
+
}
|
|
151
|
+
enable() {
|
|
152
|
+
this._enabled = true;
|
|
153
|
+
this.otel.setEnabled(true);
|
|
154
|
+
this.otel.configure();
|
|
155
|
+
this.replay.setEnabled(true);
|
|
156
|
+
this.replay.configure();
|
|
157
|
+
this.vitals.setEnabled(true);
|
|
158
|
+
this.vitals.start().catch(() => { });
|
|
159
|
+
this.errors.start();
|
|
160
|
+
this.router.start();
|
|
161
|
+
}
|
|
162
|
+
isEnabled() { return this._enabled; }
|
|
163
|
+
async destroy() {
|
|
164
|
+
this.errors.stop();
|
|
165
|
+
this.router.stop();
|
|
166
|
+
this.replay.stop();
|
|
167
|
+
this.vitals.setEnabled(false);
|
|
168
|
+
try {
|
|
169
|
+
await this.otel.shutdown();
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
console.warn("Monoscope: provider shutdown failed, some trace data may be lost", e);
|
|
173
|
+
}
|
|
174
|
+
this._enabled = false;
|
|
28
175
|
}
|
|
29
176
|
}
|
|
30
177
|
export default Monoscope;
|