@reliantlabs/forge-web-runtime 0.3.0 → 0.3.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.
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"AA+CA,IAAI,OAAO,GAAG,KAAK,CAAC;AAEpB;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,SAA0B,EAAE;IAC9D,IAAI,OAAO,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAC7C,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,GAAG,IAAI,CAAC;IAEf,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,UAAU,CAAC;IACrD,MAAM,aAAa,GAAG,MAAM,CAAC,aAAa,IAAI,IAAI,CAAC;IACnD,MAAM,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,IAAI,IAAI,CAAC;IAEzD,MAAM,QAAQ,GAAG,CAAC,KAAqB,EAAE,EAAE;QACzC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACxB,CAAC;aAAM,IACL,OAAO,OAAO,KAAK,WAAW;YAC9B,OAAO,CAAC,GAAG,EAAE,QAAQ,KAAK,aAAa,EACvC,CAAC;YACD,mEAAmE;YACnE,2CAA2C;YAC3C,OAAO,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;QAC1E,CAAC;QACD,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YACpB,WAAW,CAAC,MAAM,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC;QACnD,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,SAAS,GAAsB,EAAE,CAAC;IAExC,IAAI,aAAa,EAAE,CAAC;QAClB,MAAM,OAAO,GAAG,CAAC,CAAa,EAAE,EAAE,CAChC,QAAQ,CAAC;YACP,IAAI,EAAE,OAAO;YACb,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,IAAI,OAAO;YAC9B,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,KAAK,EAAE;YAC9D,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;SACtB,CAAC,CAAC;QACL,MAAM,WAAW,GAAG,CAAC,CAAwB,EAAE,EAAE,CAC/C,QAAQ,CAAC;YACP,IAAI,EAAE,qBAAqB;YAC3B,IAAI,EAAE,oBAAoB;YAC1B,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;YACzB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;SACtB,CAAC,CAAC;QACL,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC1C,MAAM,CAAC,gBAAgB,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAAC;QAC3D,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;QACnE,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,CAClB,MAAM,CAAC,mBAAmB,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAC9D,CAAC;IACJ,CAAC;IAED,IAAI,gBAAgB,IAAI,OAAO,mBAAmB,KAAK,UAAU,EAAE,CAAC;QAClE,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,MAAM,OAAO,GAAG,CACd,IAAY,EACZ,EAA2C,EAC3C,EAAE;YACF,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,IAAI,mBAAmB,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;gBACrE,GAAG,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;gBACtC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC;YACzC,CAAC;YAAC,MAAM,CAAC;gBACP,0DAA0D;YAC5D,CAAC;QACH,CAAC,CAAC;QAEF,OAAO,CAAC,0BAA0B,EAAE,CAAC,OAAO,EAAE,EAAE;YAC9C,MAAM,IAAI,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YAC5B,IAAI,IAAI,EAAE,CAAC;gBACT,QAAQ,CAAC;oBACP,IAAI,EAAE,WAAW;oBACjB,IAAI,EAAE,KAAK;oBACX,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC;oBACjC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;iBACtB,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO,CAAC,cAAc,EAAE,CAAC,OAAO,EAAE,EAAE;YAClC,KAAK,MAAM,KAAK,IAAI,OAA6B,EAAE,CAAC;gBAClD,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC;oBAC1B,QAAQ,IAAI,KAAK,CAAC,KAAK,CAAC;gBAC1B,CAAC;YACH,CAAC;YACD,QAAQ,CAAC;gBACP,IAAI,EAAE,WAAW;gBACjB,IAAI,EAAE,KAAK;gBACX,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;gBAClC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;aACtB,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,oEAAoE;QACpE,uEAAuE;QACvE,uEAAuE;QACvE,wEAAwE;QACxE,OAAO,CAAC,aAAa,EAAE,CAAC,OAAO,EAAE,EAAE;YACjC,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAgC,CAAC;YACxD,IAAI,KAAK,EAAE,CAAC;gBACV,QAAQ,CAAC;oBACP,IAAI,EAAE,WAAW;oBACjB,IAAI,EAAE,KAAK;oBACX,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,eAAe,GAAG,KAAK,CAAC,SAAS,CAAC;oBAC1D,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;iBACtB,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,GAAG,EAAE;QACV,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;YAC1B,CAAC,EAAE,CAAC;QACN,CAAC;QACD,OAAO,GAAG,KAAK,CAAC;IAClB,CAAC,CAAC;AACJ,CAAC;AAED,qEAAqE;AACrE,SAAS,WAAW,CAClB,QAAgB,EAChB,WAAmB,EACnB,KAAqB;IAErB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;QAChE,IAAI,OAAO,SAAS,KAAK,WAAW,IAAI,SAAS,CAAC,UAAU,EAAE,CAAC;YAC7D,SAAS,CAAC,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACvC,CAAC;aAAM,CAAC;YACN,KAAK,KAAK,CAAC,QAAQ,EAAE;gBACnB,MAAM,EAAE,MAAM;gBACd,IAAI;gBACJ,SAAS,EAAE,IAAI;gBACf,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;aAChD,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,sCAAsC;IACxC,CAAC;AACH,CAAC","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Lightweight client RUM: unhandled errors + core Web Vitals, captured with\n// native browser APIs (no extra dependency). Export is OPT-IN: by default\n// events go to an in-process sink (console.debug in dev). Set `endpoint` to\n// beacon them to a browser-reachable OTLP/HTTP collector (needs CORS on that\n// collector — stand it up before relying on export). This is the config seam;\n// distributed tracing itself (the traceparent on every RPC) is always on and\n// lives in trace.ts — it does NOT depend on this module.\nexport interface TelemetryEvent {\n type: \"error\" | \"unhandled-rejection\" | \"web-vital\";\n name: string;\n value?: number;\n message?: string;\n detail?: Record<string, unknown>;\n timestamp: number;\n}\n\nexport interface TelemetryConfig {\n serviceName?: string;\n /** OTLP/HTTP-ish collector URL. Unset = no network export (default). */\n endpoint?: string;\n /** In-process sink for every event. Default: console.debug in dev. */\n onEvent?: (event: TelemetryEvent) => void;\n /** Capture window errors + unhandled rejections. Default true. */\n captureErrors?: boolean;\n /** Capture LCP / CLS / FID via PerformanceObserver. Default true. */\n captureWebVitals?: boolean;\n}\n\ninterface LayoutShiftEntry extends PerformanceEntry {\n value: number;\n hadRecentInput: boolean;\n}\n\ninterface FirstInputEntry extends PerformanceEntry {\n processingStart: number;\n}\n\n// `process` is a bundler-injected global (Next replaces the literal\n// `process.env.NODE_ENV` at build time), not a browser one. Declaring it\n// locally keeps this package free of a @types/node dependency while leaving\n// the expression textually intact so that replacement still happens.\ndeclare const process:\n | { env?: Record<string, string | undefined> }\n | undefined;\n\nlet started = false;\n\n/**\n * initClientTelemetry wires the capture listeners and returns a teardown\n * function. Idempotent and SSR-safe (no-op without a DOM). Call once from the\n * app shell (e.g. inside Providers' mount effect).\n */\nexport function initClientTelemetry(config: TelemetryConfig = {}): () => void {\n if (started || typeof window === \"undefined\") {\n return () => {};\n }\n started = true;\n\n const serviceName = config.serviceName ?? \"frontend\";\n const captureErrors = config.captureErrors ?? true;\n const captureWebVitals = config.captureWebVitals ?? true;\n\n const dispatch = (event: TelemetryEvent) => {\n if (config.onEvent) {\n config.onEvent(event);\n } else if (\n typeof process !== \"undefined\" &&\n process.env?.NODE_ENV === \"development\"\n ) {\n // The `typeof` guard keeps this framework-agnostic — Vite SPAs and\n // plain browsers have no `process` at all.\n console.debug(\"[telemetry]\", event.type, event.name, event.value ?? \"\");\n }\n if (config.endpoint) {\n exportEvent(config.endpoint, serviceName, event);\n }\n };\n\n const teardowns: Array<() => void> = [];\n\n if (captureErrors) {\n const onError = (e: ErrorEvent) =>\n dispatch({\n type: \"error\",\n name: e.error?.name ?? \"Error\",\n message: e.message,\n detail: { filename: e.filename, line: e.lineno, col: e.colno },\n timestamp: Date.now(),\n });\n const onRejection = (e: PromiseRejectionEvent) =>\n dispatch({\n type: \"unhandled-rejection\",\n name: \"UnhandledRejection\",\n message: String(e.reason),\n timestamp: Date.now(),\n });\n window.addEventListener(\"error\", onError);\n window.addEventListener(\"unhandledrejection\", onRejection);\n teardowns.push(() => window.removeEventListener(\"error\", onError));\n teardowns.push(() =>\n window.removeEventListener(\"unhandledrejection\", onRejection),\n );\n }\n\n if (captureWebVitals && typeof PerformanceObserver === \"function\") {\n let clsValue = 0;\n const observe = (\n type: string,\n cb: (entries: PerformanceEntryList) => void,\n ) => {\n try {\n const obs = new PerformanceObserver((list) => cb(list.getEntries()));\n obs.observe({ type, buffered: true });\n teardowns.push(() => obs.disconnect());\n } catch {\n // Unsupported entry type in this browser — skip silently.\n }\n };\n\n observe(\"largest-contentful-paint\", (entries) => {\n const last = entries.at(-1);\n if (last) {\n dispatch({\n type: \"web-vital\",\n name: \"LCP\",\n value: Math.round(last.startTime),\n timestamp: Date.now(),\n });\n }\n });\n\n observe(\"layout-shift\", (entries) => {\n for (const entry of entries as LayoutShiftEntry[]) {\n if (!entry.hadRecentInput) {\n clsValue += entry.value;\n }\n }\n dispatch({\n type: \"web-vital\",\n name: \"CLS\",\n value: Number(clsValue.toFixed(4)),\n timestamp: Date.now(),\n });\n });\n\n // FID (First Input Delay): processingStart − startTime of the first\n // input. This is FID, NOT INP — INP needs the full interaction-latency\n // model from the web-vitals library, which this dependency-free module\n // deliberately doesn't pull in. Reach for `web-vitals` if you need INP.\n observe(\"first-input\", (entries) => {\n const first = entries[0] as FirstInputEntry | undefined;\n if (first) {\n dispatch({\n type: \"web-vital\",\n name: \"FID\",\n value: Math.round(first.processingStart - first.startTime),\n timestamp: Date.now(),\n });\n }\n });\n }\n\n return () => {\n for (const t of teardowns) {\n t();\n }\n started = false;\n };\n}\n\n/** Best-effort beacon of one event. Never throws into the caller. */\nfunction exportEvent(\n endpoint: string,\n serviceName: string,\n event: TelemetryEvent,\n): void {\n try {\n const body = JSON.stringify({ service: serviceName, ...event });\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n navigator.sendBeacon(endpoint, body);\n } else {\n void fetch(endpoint, {\n method: \"POST\",\n body,\n keepalive: true,\n headers: { \"Content-Type\": \"application/json\" },\n }).catch(() => {});\n }\n } catch {\n // Telemetry must never break the app.\n }\n}\n"]}
1
+ {"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"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","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Lightweight client RUM: unhandled errors + core Web Vitals, captured with\n// native browser APIs (no extra dependency). Export is OPT-IN: by default\n// events go to an in-process sink (console.debug in dev). Set `endpoint` to\n// beacon them to a browser-reachable OTLP/HTTP collector (needs CORS on that\n// collector — stand it up before relying on export). This is the config seam;\n// distributed tracing itself (the traceparent on every RPC) is always on and\n// lives in trace.ts — it does NOT depend on this module.\nexport interface TelemetryEvent {\n type: \"error\" | \"unhandled-rejection\" | \"web-vital\";\n name: string;\n value?: number;\n message?: string;\n detail?: Record<string, unknown>;\n timestamp: number;\n}\n\nexport interface TelemetryConfig {\n serviceName?: string;\n /** OTLP/HTTP-ish collector URL. Unset = no network export (default). */\n endpoint?: string;\n /** In-process sink for every event. Default: console.debug in dev. */\n onEvent?: (event: TelemetryEvent) => void;\n /** Capture window errors + unhandled rejections. Default true. */\n captureErrors?: boolean;\n /** Capture LCP / CLS / FID via PerformanceObserver. Default true. */\n captureWebVitals?: boolean;\n}\n\ninterface LayoutShiftEntry extends PerformanceEntry {\n value: number;\n hadRecentInput: boolean;\n}\n\ninterface FirstInputEntry extends PerformanceEntry {\n processingStart: number;\n}\n\n// `process` is a bundler-injected global (Next replaces the literal\n// `process.env.NODE_ENV` at build time), not a browser one. Declaring it\n// locally keeps this package free of a @types/node dependency while leaving\n// the expression textually intact so that replacement still happens.\ndeclare const process: { env?: Record<string, string | undefined> } | undefined;\n\nlet started = false;\n\n/**\n * initClientTelemetry wires the capture listeners and returns a teardown\n * function. Idempotent and SSR-safe (no-op without a DOM). Call once from the\n * app shell (e.g. inside Providers' mount effect).\n */\nexport function initClientTelemetry(config: TelemetryConfig = {}): () => void {\n if (started || typeof window === \"undefined\") {\n return () => {};\n }\n started = true;\n\n const serviceName = config.serviceName ?? \"frontend\";\n const captureErrors = config.captureErrors ?? true;\n const captureWebVitals = config.captureWebVitals ?? true;\n\n const dispatch = (event: TelemetryEvent) => {\n if (config.onEvent) {\n config.onEvent(event);\n } else if (\n typeof process !== \"undefined\" &&\n process.env?.NODE_ENV === \"development\"\n ) {\n // The `typeof` guard keeps this framework-agnostic — Vite SPAs and\n // plain browsers have no `process` at all.\n console.debug(\"[telemetry]\", event.type, event.name, event.value ?? \"\");\n }\n if (config.endpoint) {\n exportEvent(config.endpoint, serviceName, event);\n }\n };\n\n const teardowns: Array<() => void> = [];\n\n if (captureErrors) {\n const onError = (e: ErrorEvent) =>\n dispatch({\n type: \"error\",\n name: e.error?.name ?? \"Error\",\n message: e.message,\n detail: { filename: e.filename, line: e.lineno, col: e.colno },\n timestamp: Date.now(),\n });\n const onRejection = (e: PromiseRejectionEvent) =>\n dispatch({\n type: \"unhandled-rejection\",\n name: \"UnhandledRejection\",\n message: String(e.reason),\n timestamp: Date.now(),\n });\n window.addEventListener(\"error\", onError);\n window.addEventListener(\"unhandledrejection\", onRejection);\n teardowns.push(() => window.removeEventListener(\"error\", onError));\n teardowns.push(() =>\n window.removeEventListener(\"unhandledrejection\", onRejection),\n );\n }\n\n if (captureWebVitals && typeof PerformanceObserver === \"function\") {\n let clsValue = 0;\n const observe = (\n type: string,\n cb: (entries: PerformanceEntryList) => void,\n ) => {\n try {\n const obs = new PerformanceObserver((list) => cb(list.getEntries()));\n obs.observe({ type, buffered: true });\n teardowns.push(() => obs.disconnect());\n } catch {\n // Unsupported entry type in this browser — skip silently.\n }\n };\n\n observe(\"largest-contentful-paint\", (entries) => {\n const last = entries.at(-1);\n if (last) {\n dispatch({\n type: \"web-vital\",\n name: \"LCP\",\n value: Math.round(last.startTime),\n timestamp: Date.now(),\n });\n }\n });\n\n observe(\"layout-shift\", (entries) => {\n for (const entry of entries as LayoutShiftEntry[]) {\n if (!entry.hadRecentInput) {\n clsValue += entry.value;\n }\n }\n dispatch({\n type: \"web-vital\",\n name: \"CLS\",\n value: Number(clsValue.toFixed(4)),\n timestamp: Date.now(),\n });\n });\n\n // FID (First Input Delay): processingStart − startTime of the first\n // input. This is FID, NOT INP — INP needs the full interaction-latency\n // model from the web-vitals library, which this dependency-free module\n // deliberately doesn't pull in. Reach for `web-vitals` if you need INP.\n observe(\"first-input\", (entries) => {\n const first = entries[0] as FirstInputEntry | undefined;\n if (first) {\n dispatch({\n type: \"web-vital\",\n name: \"FID\",\n value: Math.round(first.processingStart - first.startTime),\n timestamp: Date.now(),\n });\n }\n });\n }\n\n return () => {\n for (const t of teardowns) {\n t();\n }\n started = false;\n };\n}\n\n/** Best-effort beacon of one event. Never throws into the caller. */\nfunction exportEvent(\n endpoint: string,\n serviceName: string,\n event: TelemetryEvent,\n): void {\n try {\n const body = JSON.stringify({ service: serviceName, ...event });\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n navigator.sendBeacon(endpoint, body);\n } else {\n void fetch(endpoint, {\n method: \"POST\",\n body,\n keepalive: true,\n headers: { \"Content-Type\": \"application/json\" },\n }).catch(() => {});\n }\n } catch {\n // Telemetry must never break the app.\n }\n}\n"]}
package/package.json CHANGED
@@ -1,8 +1,18 @@
1
1
  {
2
2
  "name": "@reliantlabs/forge-web-runtime",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "The forge frontend runtime: Connect interceptor stack, session, error boundary, toast queue, <Resource> container, W3C trace propagation and client RUM. The web twin of forge/pkg.",
5
5
  "license": "BUSL-1.1",
6
+ "//repository note": "REQUIRED for `npm publish --provenance`, not decoration. npm cross-checks this against the repository in the signed provenance statement and rejects the publish with a 422 if they disagree — an absent field reads as \"\" and fails that comparison. `directory` names the subdirectory this package lives in, so the provenance points at the real source. Omitting this is what made the first 0.3.1 publish fail AFTER the tarball had been signed and written to the sigstore transparency log.",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/reliant-labs/forge.git",
10
+ "directory": "web-runtime"
11
+ },
12
+ "homepage": "https://github.com/reliant-labs/forge/tree/main/web-runtime#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/reliant-labs/forge/issues"
15
+ },
6
16
  "//publishConfig note": "A SCOPED package defaults to restricted. Restricted breaks `npm install` for anyone outside the org — including every machine that scaffolds a project with a released forge, which resolves this package from the registry by semver range. Declaring access here means a publish cannot accidentally ship private by omitting --access public.",
7
17
  "publishConfig": {
8
18
  "access": "public"
@@ -52,39 +62,57 @@
52
62
  "@bufbuild/protobuf": "^2.5.0",
53
63
  "@connectrpc/connect": "^2.0.0",
54
64
  "@opentelemetry/api": "^1.9.0",
55
- "@opentelemetry/auto-instrumentations-web": "^0.54.0",
56
- "@opentelemetry/core": "^2.0.0",
57
- "@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
58
- "@opentelemetry/instrumentation": "^0.200.0",
59
- "@opentelemetry/resources": "^2.0.0",
60
- "@opentelemetry/sdk-trace-base": "^2.0.0",
61
- "@opentelemetry/sdk-trace-web": "^2.0.0",
65
+ "@opentelemetry/auto-instrumentations-web": "^0.67.0",
66
+ "@opentelemetry/core": "^2.8.0",
67
+ "@opentelemetry/exporter-trace-otlp-http": "^0.222.0",
68
+ "@opentelemetry/instrumentation": "^0.222.0",
69
+ "@opentelemetry/resources": "^2.8.0",
70
+ "@opentelemetry/sdk-trace-base": "^2.8.0",
71
+ "@opentelemetry/sdk-trace-web": "^2.8.0",
62
72
  "@opentelemetry/semantic-conventions": "^1.28.0",
63
73
  "@tanstack/react-query": "^5.59.0",
64
74
  "react": "^18.3.0 || ^19.0.0"
65
75
  },
66
76
  "peerDependenciesMeta": {
67
- "@tanstack/react-query": { "optional": true },
68
- "@opentelemetry/auto-instrumentations-web": { "optional": true },
69
- "@opentelemetry/core": { "optional": true },
70
- "@opentelemetry/exporter-trace-otlp-http": { "optional": true },
71
- "@opentelemetry/instrumentation": { "optional": true },
72
- "@opentelemetry/resources": { "optional": true },
73
- "@opentelemetry/sdk-trace-base": { "optional": true },
74
- "@opentelemetry/sdk-trace-web": { "optional": true },
75
- "@opentelemetry/semantic-conventions": { "optional": true }
77
+ "@tanstack/react-query": {
78
+ "optional": true
79
+ },
80
+ "@opentelemetry/auto-instrumentations-web": {
81
+ "optional": true
82
+ },
83
+ "@opentelemetry/core": {
84
+ "optional": true
85
+ },
86
+ "@opentelemetry/exporter-trace-otlp-http": {
87
+ "optional": true
88
+ },
89
+ "@opentelemetry/instrumentation": {
90
+ "optional": true
91
+ },
92
+ "@opentelemetry/resources": {
93
+ "optional": true
94
+ },
95
+ "@opentelemetry/sdk-trace-base": {
96
+ "optional": true
97
+ },
98
+ "@opentelemetry/sdk-trace-web": {
99
+ "optional": true
100
+ },
101
+ "@opentelemetry/semantic-conventions": {
102
+ "optional": true
103
+ }
76
104
  },
77
105
  "devDependencies": {
78
106
  "@bufbuild/protobuf": "^2.5.0",
79
107
  "@connectrpc/connect": "^2.0.0",
80
108
  "@opentelemetry/api": "^1.9.0",
81
- "@opentelemetry/auto-instrumentations-web": "^0.54.0",
82
- "@opentelemetry/core": "^2.0.0",
83
- "@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
84
- "@opentelemetry/instrumentation": "^0.200.0",
85
- "@opentelemetry/resources": "^2.0.0",
86
- "@opentelemetry/sdk-trace-base": "^2.0.0",
87
- "@opentelemetry/sdk-trace-web": "^2.0.0",
109
+ "@opentelemetry/auto-instrumentations-web": "^0.67.0",
110
+ "@opentelemetry/core": "^2.8.0",
111
+ "@opentelemetry/exporter-trace-otlp-http": "^0.222.0",
112
+ "@opentelemetry/instrumentation": "^0.222.0",
113
+ "@opentelemetry/resources": "^2.8.0",
114
+ "@opentelemetry/sdk-trace-base": "^2.8.0",
115
+ "@opentelemetry/sdk-trace-web": "^2.8.0",
88
116
  "@opentelemetry/semantic-conventions": "^1.28.0",
89
117
  "@tanstack/react-query": "^5.59.0",
90
118
  "@types/node": "^22.14.0",
@@ -92,7 +120,7 @@
92
120
  "@types/react-18": "npm:@types/react@~18.3.0",
93
121
  "react": "^19.1.0",
94
122
  "typescript": "^5.8.0",
95
- "vitest": "^3.1.0"
123
+ "vitest": "^4.1.11"
96
124
  },
97
125
  "engines": {
98
126
  "node": ">=22"