@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 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 **frontend logging** to your web applications.
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 logs and errors** with full context for faster debugging.
12
-
13
- With the sdk, you can seamlessly monitor how users interact with your app, measure performance, and gain insights into issues — all in one place.
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
- // ...other configuration options
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 | Type | Description |
54
- | ------------------------------ | --------------------- | ---------------------------------------------------------------------------- |
55
- | `projectId` | `string` | **Required** – Your Monoscope project ID. |
56
- | `serviceName` | `string` | **Required** – Name of your service or application. |
57
- | `exporterEndpoint` | `string` | Endpoint for exporting traces/logs. Defaults to Monoscope's ingest endpoint. |
58
- | `propagateTraceHeaderCorsUrls` | `RegExp[]` | Array of regex patterns for URLs where trace headers should be propagated. |
59
- | `resourceAttributes` | `Record<string, any>` | Additional resource-level attributes. |
60
- | `instrumentations` | `any[]` | OpenTelemetry instrumentations to enable. |
61
- | `replayEventsBaseUrl` | `string` | Base URL for session replay events. Defaults to Monoscope's ingest endpoint. |
62
- | `user` | `MonoscopeUser` | Default user information for the session. |
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 | Type | Description |
71
- | ---------- | ---------- | ------------------------- |
72
- | `email` | `string` | User's email address. |
73
- | `fullName` | `string` | User's full name. |
74
- | `name` | `string` | User's preferred name. |
75
- | `id` | `string` | User's unique identifier. |
76
- | `roles` | `string[]` | User's 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
- Retrieves the current session ID — useful for tagging custom spans or events.
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
- const sessionId = monoscope.getSessionId();
101
- console.log(sessionId);
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
+ }
@@ -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: MonoscopeConfig;
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 { v4 as uuidv4 } from "uuid";
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
- const storedSessionId = sessionStorage.getItem("monoscope-session-id");
10
- if (storedSessionId) {
11
- this.sessionId = storedSessionId;
65
+ catch (e) {
66
+ if (this.config.debug)
67
+ console.warn("Monoscope: failed to persist activity", e);
12
68
  }
13
- else {
14
- this.sessionId = uuidv4();
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
- this.config = config;
18
- this.replay = new MonoscopeReplay(config, this.sessionId);
19
- this.otel = new OpenTelemetryManager(config, this.sessionId);
20
- this.otel.configure();
21
- this.replay.configure();
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
- getSessionId() {
24
- return this.sessionId;
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;