@monoscopetech/browser 0.7.2 → 0.9.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
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
- 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.
5
+ When used together with the [Monoscope Server SDKs](https://monoscope.tech/docs/sdks/), you gain **end-to-end observability** — seamlessly connecting user interactions in the browser to backend services, APIs, and databases.
6
6
 
7
7
  This means you can:
8
8
 
@@ -32,14 +32,13 @@ Or include it directly in your HTML using a `<script>` tag:
32
32
 
33
33
  ## Quick Start
34
34
 
35
- Initialize Monoscope with your **project ID** and configuration:
35
+ Initialize Monoscope with your **API key** (found in your project settings):
36
36
 
37
37
  ```javascript
38
38
  import Monoscope from "@monoscopetech/browser";
39
39
 
40
40
  const monoscope = new Monoscope({
41
- projectId: "YOUR_PROJECT_ID",
42
- serviceName: "my-web-app",
41
+ apiKey: "YOUR_API_KEY",
43
42
  });
44
43
 
45
44
  // Identify the current user
@@ -49,6 +48,13 @@ monoscope.setUser({
49
48
  });
50
49
  ```
51
50
 
51
+ On `localhost`, debug mode is auto-enabled — you'll see a status overlay and console diagnostics. Call `monoscope.test()` to verify your setup:
52
+
53
+ ```javascript
54
+ const result = await monoscope.test();
55
+ console.log(result); // { success: true, message: "Test span sent successfully." }
56
+ ```
57
+
52
58
  ---
53
59
 
54
60
  ## Configuration
@@ -57,8 +63,9 @@ The `Monoscope` constructor accepts the following options:
57
63
 
58
64
  | Name | Type | Description |
59
65
  | --- | --- | --- |
60
- | `projectId` | `string` | **Required** – Your Monoscope project ID. |
61
- | `serviceName` | `string` | **Required** Name of your service or application. |
66
+ | `apiKey` | `string` | **Required** – Your Monoscope API key (found in project settings). |
67
+ | `serviceName` | `string` | Service name. Defaults to `location.hostname`. |
68
+ | `projectId` | `string` | Deprecated alias for `apiKey`. |
62
69
  | `exporterEndpoint` | `string` | Endpoint for exporting traces. Defaults to Monoscope's ingest endpoint. |
63
70
  | `propagateTraceHeaderCorsUrls` | `RegExp[]` | URL patterns where trace context headers should be propagated. Defaults to same-origin only. |
64
71
  | `resourceAttributes` | `Record<string, string>` | Additional OpenTelemetry resource attributes. |
@@ -134,6 +141,15 @@ Returns the current session ID.
134
141
 
135
142
  Returns the current tab ID (unique per browser tab).
136
143
 
144
+ ### `test(): Promise<{success: boolean, message: string}>`
145
+
146
+ Sends a test span and flushes it to verify the connection is working.
147
+
148
+ ```javascript
149
+ const result = await monoscope.test();
150
+ console.log(result.success); // true if the span was accepted
151
+ ```
152
+
137
153
  ### `enable()` / `disable()`
138
154
 
139
155
  Dynamically enable or disable all data collection.
@@ -167,7 +183,7 @@ import { MonoscopeProvider, useMonoscope, useMonoscopeUser, MonoscopeErrorBounda
167
183
  // Wrap your app with MonoscopeProvider
168
184
  function App() {
169
185
  return (
170
- <MonoscopeProvider config={{ projectId: "YOUR_PROJECT_ID", serviceName: "my-react-app" }}>
186
+ <MonoscopeProvider config={{ apiKey: "YOUR_API_KEY" }}>
171
187
  <MonoscopeErrorBoundary fallback={<div>Something went wrong</div>}>
172
188
  <MyApp />
173
189
  </MonoscopeErrorBoundary>
@@ -261,8 +277,7 @@ Pass extra OTel instrumentations via the `instrumentations` config to extend tra
261
277
  import { LongTaskInstrumentation } from "@opentelemetry/instrumentation-long-task";
262
278
 
263
279
  const monoscope = new Monoscope({
264
- projectId: "YOUR_PROJECT_ID",
265
- serviceName: "my-app",
280
+ apiKey: "YOUR_API_KEY",
266
281
  instrumentations: [new LongTaskInstrumentation()],
267
282
  });
268
283
  ```
package/dist/errors.js CHANGED
@@ -21,7 +21,15 @@ export class ErrorTracker {
21
21
  createErrorSpan(spanName, errorType, attrs) {
22
22
  this.errorCount++;
23
23
  const crumbs = getBreadcrumbs();
24
- this.emit(spanName, { "error.type": errorType, "error.count": this.errorCount, ...attrs }, (s) => {
24
+ const name = String(attrs["error.name"] || errorType);
25
+ const msg = String(attrs["error.message"] || "").replace(/\s+/g, " ").slice(0, 80);
26
+ this.emit(spanName, {
27
+ "error.type": errorType,
28
+ "error.count": this.errorCount,
29
+ "monoscope.kind": "error",
30
+ "monoscope.display.label": msg ? `${name} · ${msg}` : name,
31
+ ...attrs,
32
+ }, (s) => {
25
33
  s.setStatus({ code: SpanStatusCode.ERROR });
26
34
  if (crumbs.length > 0)
27
35
  s.setAttribute("breadcrumbs", safeStringify(crumbs));
package/dist/index.d.ts CHANGED
@@ -13,8 +13,11 @@ declare class Monoscope {
13
13
  private vitals;
14
14
  private router;
15
15
  private _enabled;
16
+ private overlay;
16
17
  constructor(config: MonoscopeConfig);
18
+ private logInitBanner;
17
19
  private resolveSessionId;
20
+ private resolveTabId;
18
21
  private persistActivity;
19
22
  private rotateSession;
20
23
  private checkAndRefreshSession;
@@ -24,6 +27,10 @@ declare class Monoscope {
24
27
  setUser(u: MonoscopeUser): void;
25
28
  startSpan<T>(name: string, fn: (span: Span) => T): T;
26
29
  recordEvent(name: string, attributes?: Record<string, string | number | boolean>): void;
30
+ test(): Promise<{
31
+ success: boolean;
32
+ message: string;
33
+ }>;
27
34
  disable(): void;
28
35
  enable(): void;
29
36
  isEnabled(): boolean;
package/dist/index.js CHANGED
@@ -1,22 +1,32 @@
1
1
  import { MonoscopeReplay } from "./replay";
2
- import { OpenTelemetryManager } from "./tracing";
2
+ import { OpenTelemetryManager, newId } from "./tracing";
3
3
  import { ErrorTracker } from "./errors";
4
4
  import { WebVitalsCollector } from "./web-vitals";
5
5
  import { SPARouter } from "./router";
6
6
  import { addBreadcrumb, clearBreadcrumbs } from "./breadcrumbs";
7
+ import { DevOverlay } from "./overlay";
7
8
  const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
8
9
  const LAST_ACTIVITY_KEY = "monoscope-last-activity";
9
10
  const isBrowser = typeof window !== "undefined";
10
11
  class Monoscope {
11
12
  constructor(config) {
12
- if (!config.projectId)
13
- throw new Error("MonoscopeConfig must include projectId");
13
+ this.overlay = null;
14
+ const resolvedKey = config.apiKey || config.projectId;
15
+ if (!resolvedKey)
16
+ throw new Error("MonoscopeConfig must include apiKey (or projectId)");
17
+ if (config.projectId && !config.apiKey && config.debug) {
18
+ console.warn("[Monoscope] `projectId` is deprecated. Use `apiKey` instead.");
19
+ }
20
+ config = { ...config, apiKey: resolvedKey, projectId: resolvedKey };
21
+ const isLocalhost = isBrowser && (location.hostname === "localhost" || location.hostname === "127.0.0.1");
22
+ if (config.debug === undefined && isLocalhost)
23
+ config = { ...config, debug: true };
14
24
  if (!config.serviceName)
15
- throw new Error("MonoscopeConfig must include serviceName");
25
+ config = { ...config, serviceName: isBrowser ? location.hostname : "unknown" };
16
26
  this.config = config;
17
27
  this._enabled = config.enabled !== false;
18
- this.tabId = crypto.randomUUID();
19
- this.sessionId = isBrowser ? this.resolveSessionId() : crypto.randomUUID();
28
+ this.tabId = isBrowser ? this.resolveTabId() : newId();
29
+ this.sessionId = isBrowser ? this.resolveSessionId() : newId();
20
30
  this.lastActivityTime = Date.now();
21
31
  if (isBrowser)
22
32
  this.persistActivity();
@@ -25,7 +35,7 @@ class Monoscope {
25
35
  const emit = (...args) => this.otel.emitSpan(...args);
26
36
  this.errors = new ErrorTracker(emit);
27
37
  this.vitals = new WebVitalsCollector(emit);
28
- this.router = new SPARouter(emit);
38
+ this.router = new SPARouter((from, to, method) => this.otel.startRouteChange(from, to, method));
29
39
  if (this._enabled) {
30
40
  this.otel.configure();
31
41
  this.replay.configure();
@@ -36,9 +46,32 @@ class Monoscope {
36
46
  });
37
47
  this.router.start();
38
48
  }
49
+ if (this._enabled && this.config.debug) {
50
+ this.logInitBanner();
51
+ if (isBrowser) {
52
+ this.overlay = new DevOverlay();
53
+ this.otel.onExportStatus = (ok) => this.overlay?.setConnectionStatus(ok);
54
+ this.otel.onSpanStart = () => this.overlay?.incrementEvents();
55
+ }
56
+ }
39
57
  if (isBrowser)
40
58
  this.setupActivityTracking();
41
59
  }
60
+ logInitBanner() {
61
+ const c = this.config;
62
+ const endpoint = c.exporterEndpoint || "https://otelcol.monoscope.tech/v1/traces";
63
+ const samplePct = Math.round((c.sampleRate ?? 1) * 100);
64
+ const replayPct = Math.round((c.replaySampleRate ?? 1) * 100);
65
+ console.groupCollapsed("%c[Monoscope] ✓ Initialized", "color: #22c55e; font-weight: bold");
66
+ console.log(` API Key: ${c.apiKey}`);
67
+ console.log(` Service: ${c.serviceName}`);
68
+ console.log(` Session: ${this.sessionId}`);
69
+ console.log(` Tracing: ✓ (sample rate: ${samplePct}%)`);
70
+ console.log(` Replay: ✓ (sample rate: ${replayPct}%)`);
71
+ console.log(` Errors: ✓`);
72
+ console.log(` Endpoint: ${endpoint}`);
73
+ console.groupEnd();
74
+ }
42
75
  resolveSessionId() {
43
76
  try {
44
77
  const storedId = sessionStorage.getItem("monoscope-session-id");
@@ -48,14 +81,29 @@ class Monoscope {
48
81
  if (elapsed < SESSION_TIMEOUT_MS)
49
82
  return storedId;
50
83
  }
51
- const newId = crypto.randomUUID();
52
- sessionStorage.setItem("monoscope-session-id", newId);
53
- return newId;
84
+ const id = newId();
85
+ sessionStorage.setItem("monoscope-session-id", id);
86
+ return id;
54
87
  }
55
88
  catch (e) {
56
89
  if (this.config.debug)
57
90
  console.warn("Monoscope: sessionStorage unavailable, using ephemeral session", e);
58
- return crypto.randomUUID();
91
+ return newId();
92
+ }
93
+ }
94
+ // sessionStorage is tab-scoped, so a persisted id naturally identifies a
95
+ // single tab across MPA navigations and SPA reloads.
96
+ resolveTabId() {
97
+ try {
98
+ const existing = sessionStorage.getItem("monoscope-tab-id");
99
+ if (existing)
100
+ return existing;
101
+ const id = newId();
102
+ sessionStorage.setItem("monoscope-tab-id", id);
103
+ return id;
104
+ }
105
+ catch {
106
+ return newId();
59
107
  }
60
108
  }
61
109
  persistActivity() {
@@ -73,7 +121,7 @@ class Monoscope {
73
121
  console.warn("Monoscope: failed to save replay on session rotation", e);
74
122
  });
75
123
  clearBreadcrumbs();
76
- this.sessionId = crypto.randomUUID();
124
+ this.sessionId = newId();
77
125
  try {
78
126
  sessionStorage.setItem("monoscope-session-id", this.sessionId);
79
127
  }
@@ -140,6 +188,22 @@ class Monoscope {
140
188
  recordEvent(name, attributes) {
141
189
  this.otel.recordEvent(name, attributes);
142
190
  }
191
+ async test() {
192
+ try {
193
+ this.otel.emitSpan("monoscope.test", { "test.timestamp": Date.now() });
194
+ await this.otel.forceFlush();
195
+ const msg = "Test span sent successfully.";
196
+ if (this.config.debug)
197
+ console.log(`[Monoscope] ${msg}`);
198
+ return { success: true, message: msg };
199
+ }
200
+ catch (e) {
201
+ const msg = `Test failed: ${e}`;
202
+ if (this.config.debug)
203
+ console.error(`[Monoscope] ${msg}`);
204
+ return { success: false, message: msg };
205
+ }
206
+ }
143
207
  disable() {
144
208
  this._enabled = false;
145
209
  this.replay.setEnabled(false);