@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/dist/tracing.js CHANGED
@@ -5,90 +5,243 @@ import { ZoneContextManager } from "@opentelemetry/context-zone";
5
5
  import { registerInstrumentations } from "@opentelemetry/instrumentation";
6
6
  import { XMLHttpRequestInstrumentation } from "@opentelemetry/instrumentation-xml-http-request";
7
7
  import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
8
+ import { UserInteractionInstrumentation } from "@opentelemetry/instrumentation-user-interaction";
8
9
  import { W3CTraceContextPropagator } from "@opentelemetry/core";
9
10
  import { resourceFromAttributes } from "@opentelemetry/resources";
10
11
  import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
11
12
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
13
+ import { context, SpanStatusCode, trace } from "@opentelemetry/api";
14
+ const MONOSCOPE_TRACER = "monoscope";
12
15
  export class OpenTelemetryManager {
13
- constructor(config, sessionId) {
16
+ constructor(config, sessionId, tabId) {
17
+ this.longTaskObserver = null;
18
+ this.resourceObserver = null;
19
+ this._enabled = true;
20
+ this._configured = false;
21
+ this.pageSpan = null;
22
+ this.pageContext = null;
23
+ this.endPageSpanHandler = null;
14
24
  this.config = config;
15
25
  this.sessionId = sessionId;
26
+ this.tabId = tabId;
16
27
  this.provider = this.createProvider();
17
28
  }
18
29
  createProvider() {
19
- const { serviceName, resourceAttributes, exporterEndpoint, projectId } = this.config;
20
- const resource = resourceFromAttributes({
21
- [ATTR_SERVICE_NAME]: serviceName,
22
- "at-project-id": projectId,
23
- ...(resourceAttributes || {}),
24
- });
25
- const otlpExporter = new OTLPTraceExporter({
26
- url: exporterEndpoint || "https://otelcol.apitoolkit.io/v1/traces",
27
- headers: {},
28
- });
30
+ const { serviceName, resourceAttributes = {}, exporterEndpoint, projectId } = this.config;
29
31
  return new WebTracerProvider({
30
- resource,
31
- spanProcessors: [new BatchSpanProcessor(otlpExporter)],
32
+ resource: resourceFromAttributes({
33
+ [ATTR_SERVICE_NAME]: serviceName,
34
+ "at-project-id": projectId,
35
+ ...resourceAttributes,
36
+ }),
37
+ spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({
38
+ url: exporterEndpoint || "https://otelcol.apitoolkit.io/v1/traces",
39
+ headers: {},
40
+ }))],
32
41
  });
33
42
  }
43
+ commonAttrs() {
44
+ const attrs = {
45
+ "session.id": this.sessionId,
46
+ "tab.id": this.tabId,
47
+ "page.url": location.href,
48
+ "page.title": document.title,
49
+ };
50
+ if (document.referrer)
51
+ attrs["page.referrer"] = document.referrer;
52
+ return attrs;
53
+ }
54
+ applyCommonAttrs(span) {
55
+ for (const [k, v] of Object.entries(this.commonAttrs()))
56
+ span.setAttribute(k, v);
57
+ if (this.config.user) {
58
+ for (const [k, v] of Object.entries(this.config.user)) {
59
+ if (v !== undefined)
60
+ span.setAttribute(`user.${k}`, v);
61
+ }
62
+ }
63
+ }
34
64
  configure() {
65
+ if (typeof window === "undefined" || this._configured)
66
+ return;
67
+ this._configured = true;
68
+ const rate = Math.max(0, Math.min(1, this.config.sampleRate ?? 1));
69
+ if (Math.random() >= rate) {
70
+ this._enabled = false;
71
+ if (this.config.debug)
72
+ console.log("MonoscopeOTel: sampled out");
73
+ return;
74
+ }
35
75
  this.provider.register({
36
76
  contextManager: new ZoneContextManager(),
37
77
  propagator: new W3CTraceContextPropagator(),
38
78
  });
39
- const headerUrls = this.config.propagateTraceHeaderCorsUrls || [
40
- /^https?:\/\/.*/,
41
- ];
79
+ // Default to same-origin only to avoid leaking trace context to third parties
80
+ const headerUrls = this.config.propagateTraceHeaderCorsUrls || [new RegExp(`^${location.origin}`)];
42
81
  const ignoreUrls = [
43
82
  /^https?:\/\/(?:[^\/]+\.)?apitoolkit\.io\//,
44
83
  /^https?:\/\/(?:[^\/]+\.)?monoscope\.tech\//,
45
84
  ];
85
+ const addAttrs = (span) => this.applyCommonAttrs(span);
46
86
  registerInstrumentations({
47
87
  tracerProvider: this.provider,
48
88
  instrumentations: [
49
89
  ...(this.config.instrumentations || []),
50
90
  new DocumentLoadInstrumentation({
51
91
  ignoreNetworkEvents: !this.config.enableNetworkEvents,
52
- applyCustomAttributesOnSpan: {
53
- documentLoad: (span) => {
54
- span.setAttribute("session.id", this.sessionId);
55
- this.setUserAttributes(span);
56
- },
57
- },
92
+ applyCustomAttributesOnSpan: { documentLoad: addAttrs },
58
93
  }),
59
94
  new XMLHttpRequestInstrumentation({
60
- propagateTraceHeaderCorsUrls: headerUrls,
61
- ignoreUrls,
62
- applyCustomAttributesOnSpan: (span) => {
63
- span.setAttribute("session.id", this.sessionId);
64
- this.setUserAttributes(span);
65
- },
95
+ propagateTraceHeaderCorsUrls: headerUrls, ignoreUrls, applyCustomAttributesOnSpan: addAttrs,
66
96
  }),
67
97
  new FetchInstrumentation({
68
- propagateTraceHeaderCorsUrls: headerUrls,
69
- ignoreUrls,
70
- applyCustomAttributesOnSpan: (span) => {
71
- span.setAttribute("session.id", this.sessionId);
72
- this.setUserAttributes(span);
73
- },
98
+ propagateTraceHeaderCorsUrls: headerUrls, ignoreUrls, applyCustomAttributesOnSpan: addAttrs,
74
99
  }),
100
+ ...(this.config.enableUserInteraction ? [new UserInteractionInstrumentation()] : []),
75
101
  ],
76
102
  });
103
+ this.startPageSpan();
104
+ this.observeLongTasks();
105
+ this.observeResourceTiming();
77
106
  }
78
- setUserAttributes(span) {
79
- if (this.config.user) {
80
- for (let k in this.config.user) {
81
- span.setAttribute(`user.${k}`, this.config.user[k]);
82
- }
107
+ startPageSpan() {
108
+ const tracer = trace.getTracer(MONOSCOPE_TRACER);
109
+ this.pageSpan = tracer.startSpan("browser.session", { attributes: this.commonAttrs() });
110
+ this.pageContext = trace.setSpan(context.active(), this.pageSpan);
111
+ this.endPageSpanHandler = () => { this.pageSpan?.end(); this.pageSpan = null; };
112
+ window.addEventListener("pagehide", this.endPageSpanHandler);
113
+ window.addEventListener("beforeunload", this.endPageSpanHandler);
114
+ }
115
+ getPageContext() { return this.pageContext; }
116
+ withPageContext(fn) {
117
+ if (this.pageContext)
118
+ return context.with(this.pageContext, fn);
119
+ return fn();
120
+ }
121
+ emitSpan(name, attrs, configure) {
122
+ try {
123
+ const tracer = trace.getTracer(MONOSCOPE_TRACER);
124
+ this.withPageContext(() => tracer.startActiveSpan(name, (span) => {
125
+ this.applyCommonAttrs(span);
126
+ for (const [k, v] of Object.entries(attrs))
127
+ span.setAttribute(k, v);
128
+ configure?.(span);
129
+ span.end();
130
+ }));
131
+ }
132
+ catch (e) {
133
+ if (this.config.debug)
134
+ console.warn("Monoscope: span emit failed for", name, e);
135
+ }
136
+ }
137
+ observeLongTasks() {
138
+ if (typeof PerformanceObserver === "undefined")
139
+ return;
140
+ try {
141
+ this.longTaskObserver = new PerformanceObserver((list) => {
142
+ if (!this._enabled)
143
+ return;
144
+ for (const entry of list.getEntries()) {
145
+ try {
146
+ const attrs = {
147
+ "longtask.duration": entry.duration,
148
+ "longtask.name": entry.name,
149
+ };
150
+ const attr = entry.attribution;
151
+ if (attr?.[0]?.containerSrc)
152
+ attrs["longtask.script"] = attr[0].containerSrc;
153
+ if (attr?.[0]?.containerName)
154
+ attrs["longtask.container"] = attr[0].containerName;
155
+ this.emitSpan("longtask", attrs);
156
+ }
157
+ catch (e) {
158
+ if (this.config.debug)
159
+ console.warn("Monoscope: failed to process longtask entry", e);
160
+ }
161
+ }
162
+ });
163
+ this.longTaskObserver.observe({ type: "longtask", buffered: true });
164
+ }
165
+ catch (e) {
166
+ console.warn("Monoscope: longtask observation not supported", e);
167
+ }
168
+ }
169
+ observeResourceTiming() {
170
+ if (typeof PerformanceObserver === "undefined")
171
+ return;
172
+ const threshold = this.config.resourceTimingThresholdMs ?? 200;
173
+ try {
174
+ this.resourceObserver = new PerformanceObserver((list) => {
175
+ if (!this._enabled)
176
+ return;
177
+ for (const entry of list.getEntries()) {
178
+ if (entry.duration < threshold)
179
+ continue;
180
+ try {
181
+ const re = entry;
182
+ this.emitSpan("resource", {
183
+ "resource.name": re.name,
184
+ "resource.duration": re.duration,
185
+ "resource.type": re.initiatorType,
186
+ "resource.transferSize": re.transferSize,
187
+ "resource.encodedBodySize": re.encodedBodySize,
188
+ });
189
+ }
190
+ catch (e) {
191
+ if (this.config.debug)
192
+ console.warn("Monoscope: failed to process resource entry", e);
193
+ }
194
+ }
195
+ });
196
+ this.resourceObserver.observe({ type: "resource", buffered: false });
197
+ }
198
+ catch (e) {
199
+ console.warn("Monoscope: resource timing not supported", e);
83
200
  }
84
201
  }
202
+ startSpan(name, fn) {
203
+ const tracer = trace.getTracer(MONOSCOPE_TRACER);
204
+ return this.withPageContext(() => tracer.startActiveSpan(name, (span) => {
205
+ this.applyCommonAttrs(span);
206
+ try {
207
+ const result = fn(span);
208
+ if (result instanceof Promise) {
209
+ return result.then((v) => { span.end(); return v; }, (e) => {
210
+ span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) });
211
+ span.end();
212
+ throw e;
213
+ });
214
+ }
215
+ span.end();
216
+ return result;
217
+ }
218
+ catch (e) {
219
+ span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) });
220
+ span.end();
221
+ throw e;
222
+ }
223
+ }));
224
+ }
225
+ recordEvent(name, attributes = {}) {
226
+ this.emitSpan(name, attributes);
227
+ }
228
+ updateSessionId(sessionId) { this.sessionId = sessionId; }
229
+ setEnabled(enabled) { this._enabled = enabled; }
85
230
  async shutdown() {
231
+ this.longTaskObserver?.disconnect();
232
+ this.resourceObserver?.disconnect();
233
+ if (this.endPageSpanHandler && typeof window !== "undefined") {
234
+ this.endPageSpanHandler();
235
+ window.removeEventListener("pagehide", this.endPageSpanHandler);
236
+ window.removeEventListener("beforeunload", this.endPageSpanHandler);
237
+ this.endPageSpanHandler = null;
238
+ }
239
+ this.pageSpan = null;
240
+ this.pageContext = null;
241
+ this._configured = false;
86
242
  await this.provider.shutdown();
87
243
  }
88
244
  setUser(newConfig) {
89
- this.config = {
90
- ...this.config,
91
- user: { ...this.config.user, ...newConfig },
92
- };
245
+ this.config = { ...this.config, user: { ...this.config.user, ...newConfig } };
93
246
  }
94
247
  }
@@ -1 +1 @@
1
- {"root":["../src/index.ts","../src/replay.ts","../src/tracing.ts","../src/types.ts"],"version":"5.8.3"}
1
+ {"root":["../src/breadcrumbs.ts","../src/errors.ts","../src/index.ts","../src/react.tsx","../src/replay.ts","../src/router.ts","../src/tracing.ts","../src/types.ts","../src/web-vitals.ts"],"version":"5.9.3"}
package/dist/types.d.ts CHANGED
@@ -5,11 +5,16 @@ export type MonoscopeConfig = {
5
5
  propagateTraceHeaderCorsUrls?: RegExp[];
6
6
  projectId: string;
7
7
  resourceAttributes?: Record<string, string>;
8
- instrumentations?: any[];
8
+ instrumentations?: unknown[];
9
9
  replayEventsBaseUrl?: string;
10
10
  enableNetworkEvents?: boolean;
11
11
  user?: MonoscopeUser;
12
12
  debug?: boolean;
13
+ sampleRate?: number;
14
+ replaySampleRate?: number;
15
+ enabled?: boolean;
16
+ resourceTimingThresholdMs?: number;
17
+ enableUserInteraction?: boolean;
13
18
  };
14
19
  export type MonoscopeUser = {
15
20
  email?: string;
@@ -17,7 +22,7 @@ export type MonoscopeUser = {
17
22
  name?: string;
18
23
  id?: string;
19
24
  roles?: string[];
20
- } & Record<string, string>;
25
+ } & Record<string, string | string[] | undefined>;
21
26
  declare global {
22
27
  interface Window {
23
28
  monoscope: Monoscope;
@@ -0,0 +1,10 @@
1
+ type EmitFn = (name: string, attrs: Record<string, string | number>) => void;
2
+ export declare class WebVitalsCollector {
3
+ private emit;
4
+ private _enabled;
5
+ private _active;
6
+ constructor(emit: EmitFn);
7
+ start(): Promise<void>;
8
+ setEnabled(enabled: boolean): void;
9
+ }
10
+ export {};
@@ -0,0 +1,31 @@
1
+ export class WebVitalsCollector {
2
+ constructor(emit) {
3
+ this._enabled = true;
4
+ this._active = false;
5
+ this.emit = emit;
6
+ }
7
+ async start() {
8
+ if (typeof window === "undefined" || this._active)
9
+ return;
10
+ this._active = true;
11
+ try {
12
+ const { onCLS, onINP, onLCP, onFCP, onTTFB } = await import("web-vitals");
13
+ const report = (m) => {
14
+ if (!this._enabled)
15
+ return;
16
+ this.emit(`web-vital.${m.name}`, {
17
+ "vital.name": m.name,
18
+ "vital.value": m.value,
19
+ "vital.rating": m.rating,
20
+ "vital.id": m.id,
21
+ "vital.navigationType": m.navigationType,
22
+ });
23
+ };
24
+ [onCLS, onINP, onLCP, onFCP, onTTFB].forEach(fn => fn(report));
25
+ }
26
+ catch (e) {
27
+ console.warn("Monoscope: web-vitals collection failed to initialize", e);
28
+ }
29
+ }
30
+ setEnabled(enabled) { this._enabled = enabled; }
31
+ }
package/package.json CHANGED
@@ -1,17 +1,32 @@
1
1
  {
2
2
  "name": "@monoscopetech/browser",
3
- "version": "0.6.1",
3
+ "version": "0.7.2",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
7
7
  "unpkg": "dist/monoscope.umd.js",
8
8
  "browser": "dist/monoscope.umd.js",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./react": {
16
+ "types": "./dist/react.d.ts",
17
+ "import": "./dist/react.js",
18
+ "default": "./dist/react.js"
19
+ }
20
+ },
9
21
  "scripts": {
10
22
  "build": "npx tsc --build && rollup -c",
23
+ "test": "vitest run",
24
+ "test:e2e": "playwright test",
11
25
  "lint": "eslint src --ext .ts",
12
26
  "prepublishOnly": "pnpm run build"
13
27
  },
14
28
  "dependencies": {
29
+ "@opentelemetry/api": "^1.9.0",
15
30
  "@opentelemetry/context-zone": "^2.0.1",
16
31
  "@opentelemetry/core": "^2.0.1",
17
32
  "@opentelemetry/exporter-logs-otlp-http": "^0.203.0",
@@ -20,6 +35,7 @@
20
35
  "@opentelemetry/instrumentation": "^0.203.0",
21
36
  "@opentelemetry/instrumentation-document-load": "^0.48.0",
22
37
  "@opentelemetry/instrumentation-fetch": "^0.203.0",
38
+ "@opentelemetry/instrumentation-user-interaction": "^0.58.0",
23
39
  "@opentelemetry/instrumentation-xml-http-request": "^0.203.0",
24
40
  "@opentelemetry/resources": "^2.0.1",
25
41
  "@opentelemetry/sdk-trace-base": "^2.0.1",
@@ -27,27 +43,45 @@
27
43
  "@opentelemetry/semantic-conventions": "^1.36.0",
28
44
  "@rrweb/rrweb-plugin-console-record": "2.0.0-alpha.18",
29
45
  "rrweb": "2.0.0-alpha.4",
30
- "uuid": "^11.1.0"
46
+ "web-vitals": "^4.2.0"
31
47
  },
32
48
  "files": [
33
49
  "dist"
34
50
  ],
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/monoscope-tech/monoscope-web.git"
54
+ },
35
55
  "publishConfig": {
36
- "access": "public"
56
+ "access": "public",
57
+ "provenance": true
37
58
  },
38
59
  "keywords": [],
39
60
  "author": "",
40
61
  "license": "MIT",
62
+ "peerDependencies": {
63
+ "react": ">=17"
64
+ },
65
+ "peerDependenciesMeta": {
66
+ "react": {
67
+ "optional": true
68
+ }
69
+ },
41
70
  "devDependencies": {
71
+ "@playwright/test": "^1.58.2",
42
72
  "@rollup/plugin-commonjs": "^25.0.0",
43
73
  "@rollup/plugin-json": "^6.1.0",
44
74
  "@rollup/plugin-node-resolve": "^15.0.0",
45
- "@rollup/plugin-terser": "^0.4.4",
75
+ "@rollup/plugin-terser": "^1.0.0",
46
76
  "@rollup/plugin-typescript": "^11.0.0",
47
- "@types/uuid": "^10.0.0",
77
+ "@testing-library/jest-dom": "^6.9.1",
78
+ "@testing-library/react": "^16.3.2",
79
+ "@types/react": "^19.0.0",
48
80
  "buffer": "^5.5.0||^6.0.0",
81
+ "jsdom": "^29.0.1",
49
82
  "rollup": "^4.0.0",
50
83
  "tslib": "^2.8.1",
51
- "typescript": "^5.0.0"
84
+ "typescript": "^5.0.0",
85
+ "vitest": "^4.1.1"
52
86
  }
53
87
  }