diff --git a/docs/package.json b/docs/package.json index b507ce5a..6793424d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -12,6 +12,15 @@ "dependencies": { "@docsearch/react": "3", "@lazarv/react-server": "workspace:^", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.200.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.200.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/sdk-metrics": "^2.0.0", + "@opentelemetry/sdk-node": "^0.200.0", + "@opentelemetry/sdk-trace-base": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.28.0", "@uidotdev/usehooks": "^2.4.1", "algoliasearch": "^4.24.0", "highlight.js": "^11.9.0", diff --git a/docs/react-server.development.config.json b/docs/react-server.development.config.json new file mode 100644 index 00000000..4dffdffb --- /dev/null +++ b/docs/react-server.development.config.json @@ -0,0 +1,5 @@ +{ + "telemetry": { + "enabled": true + } +} diff --git a/docs/src/pages/en/(pages)/features/observability.mdx b/docs/src/pages/en/(pages)/features/observability.mdx new file mode 100644 index 00000000..65ccdfc2 --- /dev/null +++ b/docs/src/pages/en/(pages)/features/observability.mdx @@ -0,0 +1,417 @@ +--- +title: Observability +category: Features +order: 16 +--- + +import Link from "../../../../components/Link.jsx"; + +# Observability + +`@lazarv/react-server` provides built-in [OpenTelemetry](https://opentelemetry.io/) integration for full observability in both development and production. When enabled, the runtime automatically instruments HTTP requests, SSR rendering, server functions, and middleware — emitting distributed traces and metrics without any application code changes. + +All OpenTelemetry dependencies are **optional** and loaded lazily. When telemetry is disabled (the default), there is **zero runtime overhead** — all instrumentation resolves to no-op objects. + + +## Getting Started + + +### Install OpenTelemetry packages + +```bash +pnpm add @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-http @opentelemetry/sdk-metrics @opentelemetry/core @opentelemetry/resources @opentelemetry/semantic-conventions +``` + +### Enable telemetry + +You can enable telemetry in any of these ways: + +**1. Configuration file** — add a `telemetry` section to your `react-server.config.mjs`: + +```js +export default { + telemetry: { + enabled: true, + serviceName: "my-app", + }, +}; +``` + +**2. Environment variable** — set the standard OpenTelemetry endpoint: + +```bash +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 +``` + +**3. Runtime-specific env var:** + +```bash +REACT_SERVER_TELEMETRY=true +``` + +When enabled, the runtime initializes the OpenTelemetry SDK on server startup and shuts it down gracefully when the server closes. + + +## Configuration + + +All telemetry settings live under the `telemetry` key in your configuration file: + +```js +export default { + telemetry: { + // Enable/disable telemetry (default: false) + enabled: true, + + // Service name reported to your backend (default: package name or "@lazarv/react-server") + serviceName: "my-app", + + // OTLP endpoint (default: "http://localhost:4318") + endpoint: "http://localhost:4318", + + // Exporter type: "otlp" | "console" | "dev-console" (default: auto-detected) + exporter: "otlp", + + // Sampling rate 0.0–1.0 (default: 1.0 — sample everything) + sampleRate: 1.0, + + // Metrics configuration + metrics: { + // Enable/disable metrics collection (default: true when telemetry is enabled) + enabled: true, + + // Export interval in milliseconds (default: 30000) + interval: 30000, + }, + }, +}; +``` + +### Environment variables + +The following environment variables are respected: + +| Variable | Description | +| --- | --- | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint. Setting this also enables telemetry. | +| `OTEL_SERVICE_NAME` | Service name override. | +| `REACT_SERVER_TELEMETRY` | Set to `"true"` to enable telemetry. | + +Standard OpenTelemetry environment variables (`OTEL_*`) are passed through to the SDK. + + +## Built-in Spans + + +When telemetry is enabled, the runtime automatically creates the following trace spans: + +### HTTP Request Span + +**`HTTP Request`** — Root span for every incoming HTTP request. Extracts W3C TraceContext from incoming headers and injects trace context into response headers. + +| Attribute | Description | +| --- | --- | +| `http.method` | HTTP method (GET, POST, etc.) | +| `http.url` | Full request URL | +| `http.target` | Request path | +| `http.host` | Host header | +| `http.scheme` | Protocol (http/https) | +| `http.user_agent` | User-Agent header | +| `http.status_code` | Response status code (set after response) | +| `http.response_content_type` | Response Content-Type (set after response) | +| `net.peer.ip` | Client IP address | + +### Middleware Spans + +**`Middleware: {displayName}`** — One span per middleware in the compose chain. Each span measures only the middleware's own work — calling `next()` ends the span before the next middleware runs. + +| Attribute | Description | +| --- | --- | +| `react_server.middleware.index` | Position in the middleware chain (0-based) | +| `react_server.middleware.name` | Middleware function name | +| `react_server.middleware.display_name` | Human-readable name (e.g. "CORS", "Static Files", "SSR Handler") | + +### Render Spans + +The renderer creates two nested **`Render`** spans per request: + +1. **RSC Render** — outer span wrapping the full RSC→SSR pipeline +2. **SSR Render** — inner span (child of RSC) for HTML stream rendering + +| Attribute | Description | +| --- | --- | +| `react_server.render_type` | `"RSC"` or `"SSR"` | +| `react_server.outlet` | Outlet name or `"PAGE_ROOT"` | +| `http.url` | Request URL | + +### Server Function Span + +**`Server Function`** — Span for each server function invocation. + +| Attribute | Description | +| --- | --- | +| `react_server.server_function.id` | Function identifier | +| `react_server.server_function.is_form` | Whether invoked via form submission | +| `react_server.server_function.has_error` | Whether the function produced an error (set after execution) | + +### Cache Spans + +**`Cache Lookup`** — Span for each `useCache()` call. Dynamically renamed to **`Cache Hit`** or **`Cache Miss → Recompute`** based on the result. + +| Attribute | Description | +| --- | --- | +| `react_server.cache.provider` | Cache provider name (or `"default"`) | +| `react_server.cache.ttl` | TTL value (or `"Infinity"`) | +| `react_server.cache.force` | Whether cache was force-refreshed | +| `react_server.cache.hit` | `true` on hit, `false` on miss (set after lookup) | + +### Server Startup Span + +**`Server Startup`** — Span covering server initialization (both dev and production). + +| Attribute | Description | +| --- | --- | +| `react_server.mode` | `"development"` or `"production"` | +| `react_server.root` | Application root or `"file-router"` | + +### Vite Dev Server Init Span + +**`Vite Dev Server Init`** — Span for Vite dev server creation (development only). + +| Attribute | Description | +| --- | --- | +| `react_server.vite.mode` | Vite mode | +| `react_server.vite.force` | Whether dependency optimization was forced | + +### Vite Plugin Hook Spans + +**`Vite plugin [{pluginName}].{hookName}`** — In development, every Vite plugin hook (`resolveId`, `load`, `transform`, `buildStart`, `buildEnd`, `handleHotUpdate`) is automatically instrumented. + +| Attribute | Description | +| --- | --- | +| `react_server.vite.plugin` | Plugin name | +| `react_server.vite.hook` | Hook name | +| `react_server.vite.module_id` | Module being processed (for `resolveId`, `load`, `transform`) | + + +## Built-in Metrics + + +The following metrics are automatically recorded: + +| Metric | Type | Description | +| --- | --- | --- | +| `http.server.request.duration` | Histogram (ms) | Duration of HTTP requests | +| `http.server.active_requests` | UpDownCounter | Number of in-flight HTTP requests | +| `react_server.server_function.duration` | Histogram (ms) | Duration of server function execution | +| `react_server.rsc.render.duration` | Histogram (ms) | Duration of RSC rendering | +| `react_server.dom.render.duration` | Histogram (ms) | Duration of SSR DOM rendering | +| `react_server.cache.hits` | Counter | Number of cache hits | +| `react_server.cache.misses` | Counter | Number of cache misses | + + +## Telemetry API + + +Import from `@lazarv/react-server/telemetry` to extend built-in telemetry with custom spans and metrics in your server components, server functions, or middleware. + +### `withSpan(name, attributes?, fn)` + +Execute a function within a child span: + +```js +import { withSpan } from "@lazarv/react-server/telemetry"; + +export async function fetchProducts() { + return withSpan("db.query", { "db.system": "postgres" }, async (span) => { + const rows = await db.query("SELECT * FROM products"); + span.setAttribute("db.row_count", rows.length); + return rows; + }); +} +``` + +### `getSpan()` + +Get the current request span to add attributes or events: + +```js +import { getSpan } from "@lazarv/react-server/telemetry"; + +export function MyComponent() { + const span = getSpan(); + span.addEvent("component.render", { component: "MyComponent" }); + // ... +} +``` + +### `getTracer()` + +Get the active OpenTelemetry tracer for manual span creation: + +```js +import { getTracer } from "@lazarv/react-server/telemetry"; + +const tracer = getTracer(); +const span = tracer.startSpan("custom.operation"); +try { + // ... your code +} finally { + span.end(); +} +``` + +### `getMeter()` + +Get the active OpenTelemetry meter for custom metrics: + +```js +import { getMeter } from "@lazarv/react-server/telemetry"; + +const meter = getMeter(); +const counter = meter.createCounter("my_app.api_calls", { + description: "Number of external API calls", +}); +counter.add(1, { "api.name": "stripe" }); +``` + +### `getOtelContext()` + +Get the OTel context for the current request. Useful for advanced propagation scenarios: + +```js +import { getOtelContext } from "@lazarv/react-server/telemetry"; + +const ctx = getOtelContext(); +``` + +### `injectTraceContext(headers)` + +Inject W3C trace context into outgoing headers for distributed tracing across services: + +```js +import { injectTraceContext } from "@lazarv/react-server/telemetry"; + +const headers = new Headers(); +await injectTraceContext(headers); +const res = await fetch("https://api.example.com/data", { headers }); +``` + + +## Trace Propagation + + +The runtime automatically: + +1. **Extracts** W3C TraceContext headers (`traceparent`, `tracestate`) from incoming requests +2. **Propagates** context through the middleware chain, SSR handler, and server functions +3. **Injects** trace context into outgoing response headers + +This means traces from upstream services (API gateways, load balancers) are automatically correlated with react-server traces, and downstream services can continue the trace. + + +## Dev Console Exporter + + +In development, when you enable telemetry without setting an OTLP endpoint, the runtime uses a pretty-printed console exporter that renders a compact trace tree in your terminal: + +``` + GET /about 200 45.2ms + ├─ Middleware: CORS ░ 0.3ms + ├─ Middleware: Cookies ░ 0.1ms + ├─ Middleware: SSR Handler ░░░░░░ 42.1ms + │ ├─ Render RSC ░░░░ 18.3ms + │ └─ Render SSR ░░░░░ 22.4ms + ├─ Vite plugin [vite:resolve].resolveId: ×47 ░ 3.2ms + ├─ Vite plugin [vite:css].transform: ×12 ░ 1.1ms + └─ 8 spans (<1ms) +``` + +Features: +- **Color-coded durations**: green (< 20ms), yellow (20–100ms), red (> 100ms) +- **Proportional timing bars** showing relative duration within each trace +- **Hierarchical tree** using span parent-child relationships +- **Grouped Vite spans**: fast (green) Vite plugin hook spans are grouped by name with a count; slow or errored spans are shown individually +- **Collapsed micro-spans**: spans shorter than 1ms are summarized in a single line + +To use the dev console exporter explicitly: + +```js +export default { + telemetry: { + enabled: true, + exporter: "dev-console", + }, +}; +``` + + +## Production Setup + + +### Jaeger + +Run Jaeger locally with OTLP support: + +```bash +docker run -d --name jaeger \ + -p 16686:16686 \ + -p 4318:4318 \ + jaegertracing/all-in-one:latest +``` + +Then enable telemetry: + +```bash +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 pnpm react-server start +``` + +Open `http://localhost:16686` to view traces. + +### Grafana / Tempo + +Configure your OTLP endpoint in your config: + +```js +export default { + telemetry: { + enabled: true, + endpoint: "https://tempo.grafana.net/otlp", + serviceName: "my-production-app", + }, +}; +``` + +### Honeycomb / Datadog / New Relic + +Most observability platforms support OTLP ingestion. Set the endpoint and any required headers via environment variables: + +```bash +OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io +OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=your-api-key" +``` + + +## Edge Runtime + + +Telemetry is also supported in edge runtimes (Cloudflare Workers, Netlify Edge Functions, etc.) with a lightweight tracer. Due to platform constraints, only traces are supported in edge — metrics are not available. + +When building for edge, the bundler automatically handles OpenTelemetry packages: +- **Packages installed** → bundled into the worker, OTLP export or console fallback +- **Packages not installed** → resolved to empty modules, zero overhead + +The edge telemetry uses `BasicTracerProvider` with `SimpleSpanProcessor` and attempts to use the OTLP HTTP exporter, falling back to console output when it's not available. + + +## Zero Overhead When Disabled + + +When telemetry is not enabled: + +- No OpenTelemetry packages are loaded +- All span and metric operations resolve to no-op objects +- The `withSpan()` helper simply calls your function directly +- `getTracer()` and `getMeter()` return no-op instances that discard all data + +This ensures there is no performance impact on applications that don't use telemetry. diff --git a/docs/src/pages/en/features.(index).mdx b/docs/src/pages/en/features.(index).mdx index ebdabc0d..292064f5 100644 --- a/docs/src/pages/en/features.(index).mdx +++ b/docs/src/pages/en/features.(index).mdx @@ -17,3 +17,5 @@ You can also learn about some small, but useful modes in this section, like [par You can learn about how to implement a micro-frontend architecture in the [micro-frontends](/features/micro-frontends) section. The runtime provides a set of tools to help you implement micro-frontends in your app. You can use the `RemoteComponent` component to load a micro-frontend from a remote URL and render it in your app using server-side rendering. Server-side rendering supported `iframe` fragments for React applications! The runtime also provides [Workers](/features/worker) to offload heavy computations to separate threads using the `"use worker"` directive. On the server, worker functions run in Node.js Worker Threads, and on the client, they run in Web Workers — all with transparent RSC-based serialization. Workers can return plain values, React elements with Suspense, ReadableStreams, and deferred Promises. + +For production readiness, the runtime includes built-in [Observability](/features/observability) via OpenTelemetry integration. When enabled, it automatically instruments HTTP requests, SSR rendering, server actions, and middleware with distributed traces and metrics — with zero overhead when disabled. diff --git a/examples/hello-world/package.json b/examples/hello-world/package.json index 499a2d6d..eb53c288 100644 --- a/examples/hello-world/package.json +++ b/examples/hello-world/package.json @@ -12,6 +12,15 @@ "test-env": "node -e \"console.log(process.env._)\"" }, "dependencies": { - "@lazarv/react-server": "workspace:^" + "@lazarv/react-server": "workspace:^", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.200.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.200.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/sdk-metrics": "^2.0.0", + "@opentelemetry/sdk-node": "^0.200.0", + "@opentelemetry/sdk-trace-base": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.28.0" } } diff --git a/examples/hello-world/react-server.config.mjs b/examples/hello-world/react-server.config.mjs new file mode 100644 index 00000000..bcd7d0bc --- /dev/null +++ b/examples/hello-world/react-server.config.mjs @@ -0,0 +1,5 @@ +export default { + telemetry: { + enabled: true, + }, +}; diff --git a/package.json b/package.json index 0096cc02..ac7c84ff 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "oxlint --fix" ] }, - "packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a", + "packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017", "pnpm": { "overrides": { "react-click-away-listener>react": "0.0.0-experimental-3bc2d414-20260304", @@ -81,6 +81,17 @@ "supertest>superagent": "9.0.2", "browserslist": "4.28.1", "caniuse-lite": "1.0.30001761" - } + }, + "onlyBuiltDependencies": [ + "@nestjs/core", + "@parcel/watcher", + "@swc/core", + "@vercel/speed-insights", + "better-sqlite3", + "esbuild", + "playwright-chromium", + "protobufjs", + "simple-git-hooks" + ] } } diff --git a/packages/react-server/cache/index.mjs b/packages/react-server/cache/index.mjs index 6c788d14..13dcaa72 100644 --- a/packages/react-server/cache/index.mjs +++ b/packages/react-server/cache/index.mjs @@ -16,6 +16,7 @@ import { MEMORY_CACHE_CONTEXT, PRERENDER_CACHE, } from "../server/symbols.mjs"; +import { getTracer, getOtelContext } from "../server/telemetry.mjs"; export { StorageCache, memoryDriver as default, CACHE_MISS }; @@ -88,6 +89,22 @@ export async function useCache( } const key = cache.rawCanonicalKey(keys); + const providerName = provider?.name ?? "default"; + + // ── Telemetry: cache operation span (all calls are synchronous) ── + const tracer = getTracer(); + const parentCtx = getOtelContext(); + const cacheSpan = tracer.startSpan( + "Cache Lookup", + { + attributes: { + "react_server.cache.provider": providerName, + "react_server.cache.ttl": ttl === Infinity ? "Infinity" : String(ttl), + "react_server.cache.force": force, + }, + }, + parentCtx ?? undefined + ); // HACK: concurrency workaround to avoid race condition on the lock await new Promise((resolve) => setImmediate(resolve)); @@ -105,6 +122,8 @@ export async function useCache( result = await cache.get(keys); if (force || result === CACHE_MISS) { + cacheSpan.setAttribute("react_server.cache.hit", false); + cacheSpan.updateName("Cache Miss → Recompute"); let value = promise; if (typeof import.meta.env !== "undefined" && import.meta.env.DEV) { @@ -133,6 +152,9 @@ export async function useCache( ttl: ttl ?? Infinity, provider, }); + } else { + cacheSpan.setAttribute("react_server.cache.hit", true); + cacheSpan.updateName("Cache Hit"); } lock.delete(key); @@ -140,9 +162,12 @@ export async function useCache( } catch (e) { lock.delete(key); release?.(); + cacheSpan.setStatus({ code: 2, message: e?.message }); + cacheSpan.recordException(e); error = e; } + cacheSpan.end(); if (error) throw error; return result; } diff --git a/packages/react-server/config/schema.d.ts b/packages/react-server/config/schema.d.ts index ef563070..b6db590b 100644 --- a/packages/react-server/config/schema.d.ts +++ b/packages/react-server/config/schema.d.ts @@ -665,6 +665,59 @@ export interface ServerFunctionsConfig { previousSecretFiles?: string[]; } +// ───── Telemetry config ───── + +export interface TelemetryMetricsConfig { + /** + * Enable/disable metrics collection. + * @default true (when telemetry is enabled) + */ + enabled?: boolean; + + /** + * Metrics export interval in milliseconds. + * @default 30000 + */ + interval?: number; +} + +export interface TelemetryConfig { + /** + * Enable/disable telemetry. + * Also enabled by OTEL_EXPORTER_OTLP_ENDPOINT or REACT_SERVER_TELEMETRY env vars. + */ + enabled?: boolean; + + /** + * Service name reported to your observability backend. + * @default package name or "@lazarv/react-server" + */ + serviceName?: string; + + /** + * OTLP collector endpoint. + * @default "http://localhost:4318" + */ + endpoint?: string; + + /** + * Exporter type. + * @default auto-detected + */ + exporter?: "otlp" | "console" | "dev-console"; + + /** + * Sampling rate 0.0–1.0. + * @default 1.0 + */ + sampleRate?: number; + + /** + * Metrics sub-configuration. + */ + metrics?: TelemetryMetricsConfig; +} + // ───── MDX config ───── export interface MdxConfig { @@ -1006,6 +1059,13 @@ export interface ReactServerConfig { */ serverFunctions?: ServerFunctionsConfig; + /** + * OpenTelemetry observability configuration. + * When enabled, the runtime instruments HTTP requests, rendering, server functions, and middleware. + * @example `telemetry: { enabled: true, serviceName: "my-app" }` + */ + telemetry?: TelemetryConfig; + /** * File-router layout configuration. * @example `layout: { include: ["layout.jsx"] }` diff --git a/packages/react-server/config/schema.json b/packages/react-server/config/schema.json index 4d569b83..104d48f0 100644 --- a/packages/react-server/config/schema.json +++ b/packages/react-server/config/schema.json @@ -1123,6 +1123,51 @@ }, "additionalProperties": false, "description": "MDX processing configuration." + }, + "telemetry": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable/disable telemetry. Also enabled by OTEL_EXPORTER_OTLP_ENDPOINT or REACT_SERVER_TELEMETRY env vars." + }, + "serviceName": { + "type": "string", + "description": "Service name reported to your observability backend. Default: package name or \"@lazarv/react-server\"." + }, + "endpoint": { + "type": "string", + "description": "OTLP collector endpoint. Default: \"http://localhost:4318\"." + }, + "exporter": { + "enum": ["otlp", "console", "dev-console"], + "description": "Exporter type: \"otlp\" | \"console\" | \"dev-console\". Default: auto-detected." + }, + "sampleRate": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Sampling rate 0.0–1.0. Default: 1.0 (sample everything)." + }, + "metrics": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable/disable metrics collection. Default: true when telemetry is enabled." + }, + "interval": { + "type": "integer", + "minimum": 1000, + "description": "Metrics export interval in milliseconds. Default: 30000." + } + }, + "additionalProperties": false, + "description": "Metrics sub-configuration." + } + }, + "additionalProperties": false, + "description": "OpenTelemetry observability configuration. When enabled, the runtime instruments HTTP requests, rendering, server functions, and middleware." } }, "additionalProperties": false diff --git a/packages/react-server/config/schema.mjs b/packages/react-server/config/schema.mjs index 0ad8a6c0..aa641700 100644 --- a/packages/react-server/config/schema.mjs +++ b/packages/react-server/config/schema.mjs @@ -224,6 +224,25 @@ export const DESCRIPTIONS = { "mdx.remarkPlugins": "Remark plugins for MDX processing.", "mdx.rehypePlugins": "Rehype plugins for MDX processing.", "mdx.components": "Path to the MDX components file.", + + // telemetry.* + telemetry: + "OpenTelemetry observability configuration. When enabled, the runtime instruments HTTP requests, rendering, server functions, and middleware.", + "telemetry.enabled": + "Enable/disable telemetry. Also enabled by OTEL_EXPORTER_OTLP_ENDPOINT or REACT_SERVER_TELEMETRY env vars.", + "telemetry.serviceName": + 'Service name reported to your observability backend. Default: package name or "@lazarv/react-server".', + "telemetry.endpoint": + 'OTLP collector endpoint. Default: "http://localhost:4318".', + "telemetry.exporter": + 'Exporter type: "otlp" | "console" | "dev-console". Default: auto-detected.', + "telemetry.sampleRate": + "Sampling rate 0.0\u20131.0. Default: 1.0 (sample everything).", + "telemetry.metrics": "Metrics sub-configuration.", + "telemetry.metrics.enabled": + "Enable/disable metrics collection. Default: true when telemetry is enabled.", + "telemetry.metrics.interval": + "Metrics export interval in milliseconds. Default: 30000.", }; // ───── JSON Schema generator ───── @@ -885,6 +904,45 @@ export function generateJsonSchema() { }, "mdx" ), + + // ── telemetry.* ── + telemetry: prop( + { + type: "object", + properties: { + enabled: prop({ type: "boolean" }, "telemetry.enabled"), + serviceName: prop({ type: "string" }, "telemetry.serviceName"), + endpoint: prop({ type: "string" }, "telemetry.endpoint"), + exporter: prop( + { enum: ["otlp", "console", "dev-console"] }, + "telemetry.exporter" + ), + sampleRate: prop( + { type: "number", minimum: 0, maximum: 1 }, + "telemetry.sampleRate" + ), + metrics: prop( + { + type: "object", + properties: { + enabled: prop( + { type: "boolean" }, + "telemetry.metrics.enabled" + ), + interval: prop( + { type: "integer", minimum: 1000 }, + "telemetry.metrics.interval" + ), + }, + additionalProperties: false, + }, + "telemetry.metrics" + ), + }, + additionalProperties: false, + }, + "telemetry" + ), }, additionalProperties: false, }; diff --git a/packages/react-server/config/validate.mjs b/packages/react-server/config/validate.mjs index 31c8a66d..656b95c6 100644 --- a/packages/react-server/config/validate.mjs +++ b/packages/react-server/config/validate.mjs @@ -441,6 +441,27 @@ const REACT_SERVER_SCHEMA = { }) ), + // ── telemetry.* ── + telemetry: optional( + objectShape({ + enabled: optional(is.boolean), + serviceName: optional(is.string), + endpoint: optional(is.string), + exporter: optional(enumOf("otlp", "console", "dev-console")), + sampleRate: optional( + custom((v) => is.number(v) && v >= 0 && v <= 1, "number (0 – 1)") + ), + metrics: optional( + objectShape({ + enabled: optional(is.boolean), + interval: optional( + custom((v) => Number.isInteger(v) && v >= 1000, "integer (>= 1000)") + ), + }) + ), + }) + ), + // ── File-router child config ── layout: optional(oneOf(is.object, is.function)), page: optional(oneOf(is.object, is.function)), @@ -582,6 +603,15 @@ const EXAMPLES = { serverFunctions: `serverFunctions: { secret: "my-secret-key" }`, "serverFunctions.secret": `serverFunctions: { secret: "my-secret-key" }`, "serverFunctions.secretFile": `serverFunctions: { secretFile: "./secret.pem" }`, + telemetry: `telemetry: { enabled: true, serviceName: "my-app" }`, + "telemetry.enabled": `telemetry: { enabled: true }`, + "telemetry.serviceName": `telemetry: { serviceName: "my-app" }`, + "telemetry.endpoint": `telemetry: { endpoint: "http://localhost:4318" }`, + "telemetry.exporter": `telemetry: { exporter: "otlp" } // or "dev-console" or "console"`, + "telemetry.sampleRate": `telemetry: { sampleRate: 1.0 }`, + "telemetry.metrics": `telemetry: { metrics: { enabled: true, interval: 30000 } }`, + "telemetry.metrics.enabled": `telemetry: { metrics: { enabled: true } }`, + "telemetry.metrics.interval": `telemetry: { metrics: { interval: 30000 } }`, mdx: `mdx: { remarkPlugins: [...], rehypePlugins: [...] }`, "mdx.remarkPlugins": `mdx: { remarkPlugins: [remarkGfm] }`, "mdx.rehypePlugins": `mdx: { rehypePlugins: [rehypeHighlight] }`, diff --git a/packages/react-server/lib/build/edge.mjs b/packages/react-server/lib/build/edge.mjs index 8249cf3d..5dee6c0b 100644 --- a/packages/react-server/lib/build/edge.mjs +++ b/packages/react-server/lib/build/edge.mjs @@ -6,7 +6,9 @@ import replace from "@rollup/plugin-replace"; import { build as viteBuild } from "vite"; import { forRoot } from "../../config/index.mjs"; +import { resolveTelemetryConfig } from "../../server/telemetry.mjs"; +import optionalDeps from "../plugins/optional-deps.mjs"; import * as sys from "../sys.mjs"; import customLogger from "./custom-logger.mjs"; import { fileListingReporterPlugin } from "./output-filter.mjs"; @@ -28,6 +30,11 @@ const cwd = sys.cwd(); export default async function edgeBuild(root, options) { const config = forRoot(); + // When telemetry is disabled at build time, force-empty all @opentelemetry/* + // packages so they are excluded from the edge bundle entirely. + const telemetryEnabled = resolveTelemetryConfig(config) !== null; + const otelForceEmpty = telemetryEnabled ? [] : [/^@opentelemetry\//]; + const viteConfigEdge = { root: cwd, configFile: false, @@ -170,9 +177,13 @@ export default async function edgeBuild(root, options) { if (id.startsWith("node:") || /manifest\.json/.test(id)) { return true; } + // @opentelemetry/* is NOT externalized — edge runtimes have no + // node_modules. The optionalDeps plugin with forceEmpty resolves + // them to empty modules when telemetry is disabled. return false; }, plugins: [ + optionalDeps([/^@opentelemetry\//], { forceEmpty: otelForceEmpty }), replace({ preventAssignment: true, "import.meta.url": JSON.stringify("file:///C:/worker.mjs"), @@ -287,6 +298,7 @@ export default async function edgeBuild(root, options) { }, }, plugins: [ + optionalDeps([/^@opentelemetry\//], { forceEmpty: otelForceEmpty }), { name: "react-server:edge", enforce: "pre", diff --git a/packages/react-server/lib/build/server.mjs b/packages/react-server/lib/build/server.mjs index e8eb2e4c..f558c6ff 100644 --- a/packages/react-server/lib/build/server.mjs +++ b/packages/react-server/lib/build/server.mjs @@ -8,8 +8,10 @@ import glob from "fast-glob"; import { build as viteBuild } from "vite"; import { forRoot } from "../../config/index.mjs"; +import { resolveTelemetryConfig } from "../../server/telemetry.mjs"; import configPrebuilt from "../plugins/config-prebuilt.mjs"; import fileRouter from "../plugins/file-router/plugin.mjs"; +import optionalDeps from "../plugins/optional-deps.mjs"; import fixEsbuildOptionsPlugin from "../plugins/fix-esbuildoptions.mjs"; import importRemotePlugin from "../plugins/import-remote.mjs"; @@ -76,6 +78,12 @@ export default async function serverBuild(root, options, clientManifestBus) { } const config = forRoot(); + + // When telemetry is disabled at build time, force-empty all @opentelemetry/* + // packages so they are excluded from edge bundles entirely. + const telemetryEnabled = resolveTelemetryConfig(config) !== null; + const otelForceEmpty = telemetryEnabled ? [] : [/^@opentelemetry\//]; + const clientManifest = new Map(); const serverManifest = new Map(); const buildPlugins = [ @@ -151,6 +159,7 @@ export default async function serverBuild(root, options, clientManifestBus) { "@lazarv/react-server/storage-cache", "@lazarv/react-server/http-context", /^@lazarv\/react-server\/dist\//, + /^@opentelemetry\//, ]); const ssrExternal = createExternal([ /manifest\.json/, @@ -162,9 +171,12 @@ export default async function serverBuild(root, options, clientManifestBus) { "@lazarv/react-server/storage-cache", "@lazarv/react-server/http-context", /^@lazarv\/react-server\/dist\//, + /^@opentelemetry\//, ]); - // Edge external - only externalize node builtins, bundle everything else + // Edge external - only externalize node builtins, bundle everything else. + // When telemetry is disabled, also externalize @opentelemetry/* so the + // bundler never traces their (heavy) dependency trees. const edgeExternal = (id, parentId, isResolved) => { if (isBuiltin(id)) { return true; @@ -177,6 +189,10 @@ export default async function serverBuild(root, options, clientManifestBus) { if (/^@lazarv\/react-server\/dist\//.test(id)) { return true; } + // Externalize @opentelemetry/* when telemetry is disabled + if (!telemetryEnabled && /^@opentelemetry\//.test(id)) { + return true; + } return false; }; @@ -710,6 +726,13 @@ export default async function serverBuild(root, options, clientManifestBus) { ); }, plugins: [ + ...(options.edge + ? [ + optionalDeps([/^@opentelemetry\//], { + forceEmpty: otelForceEmpty, + }), + ] + : []), manifestRegistry(), resolveWorkspace(), ...(options.edge ? [preloadManifestVirtual(options)] : []), @@ -996,6 +1019,13 @@ export default async function serverBuild(root, options, clientManifestBus) { ? ssrExternal : external, plugins: [ + ...(options.edge + ? [ + optionalDeps([/^@opentelemetry\//], { + forceEmpty: otelForceEmpty, + }), + ] + : []), manifestRegistry(), resolveWorkspace(), ...(options.edge ? [preloadManifestVirtual(options)] : []), diff --git a/packages/react-server/lib/dev/create-server.mjs b/packages/react-server/lib/dev/create-server.mjs index aef773a5..ba4959b9 100644 --- a/packages/react-server/lib/dev/create-server.mjs +++ b/packages/react-server/lib/dev/create-server.mjs @@ -72,6 +72,13 @@ import { getServerCors } from "../utils/server-config.mjs"; import createLogger from "./create-logger.mjs"; import { HybridEvaluator } from "./hybrid-evaluator.mjs"; import ssrHandler from "./ssr-handler.mjs"; +import telemetryHooks from "../plugins/telemetry-hooks.mjs"; +import { + resolveTelemetryConfig, + initTelemetry, + shutdownTelemetry, + getTracer, +} from "../../server/telemetry.mjs"; const cwd = sys.cwd(); const workspaceRoot = findPackageRoot(join(cwd, "..")) ?? cwd; @@ -82,6 +89,20 @@ export default async function createServer(root, options) { options.outDir = ".react-server"; } const config = getRuntime(CONFIG_CONTEXT)?.[CONFIG_ROOT]; + + // ── Telemetry: initialize OpenTelemetry SDK ── + const telemetryConfig = resolveTelemetryConfig(config); + await initTelemetry(telemetryConfig); + + // ── Telemetry: server startup span ── + const startupTracer = getTracer(); + const startupSpan = startupTracer.startSpan("Server Startup", { + attributes: { + "react_server.mode": "development", + "react_server.root": root || "file-router", + }, + }); + let worker = null; if (sys.isDeno) { const { renderProcessSpawn } = await import("./render-process-spawn.mjs"); @@ -248,6 +269,7 @@ export default async function createServer(root, options) { asset(), optimizeDeps(), reactServerLive(options.httpServer, config), + ...(telemetryConfig ? [telemetryHooks()] : []), ], cacheDir: config.cacheDir || @@ -499,7 +521,15 @@ export default async function createServer(root, options) { } } + // ── Telemetry: Vite dev server creation span ── + const viteCreateSpan = startupTracer.startSpan("Vite Dev Server Init", { + attributes: { + "react_server.vite.mode": options.mode || "development", + "react_server.vite.force": !!options.force, + }, + }); const viteDevServer = await createViteDevServer(viteConfig); + viteCreateSpan.end(); if (config.envDir !== false) { if (globalThis.__react_server_prev_env_keys__) { @@ -925,7 +955,7 @@ export default async function createServer(root, options) { }); const initialHandlers = await Promise.all([ - async (context) => { + async function devTooling(context) { if (context.url.pathname === "/__react_server_console__") { try { await handleClientConsole(context.request.body); @@ -973,7 +1003,7 @@ export default async function createServer(root, options) { } if (config.base) { - initialHandlers.unshift(async (context) => { + initialHandlers.unshift(async function basePathStrip(context) { if (context.url.pathname.startsWith(config.base)) { context.url.pathname = context.url.pathname.slice(config.base.length) || "/"; @@ -1002,6 +1032,9 @@ export default async function createServer(root, options) { }) ); + // ── Telemetry: end startup span ── + startupSpan.end(); + const localHostnames = new Set([ "localhost", "127.0.0.1", @@ -1018,6 +1051,7 @@ export default async function createServer(root, options) { }); }, close: () => { + shutdownTelemetry(); viteDevServer.close(); }, ws: viteDevServer.environments.client.hot, diff --git a/packages/react-server/lib/dev/ssr-handler.mjs b/packages/react-server/lib/dev/ssr-handler.mjs index eb837f27..6092585e 100644 --- a/packages/react-server/lib/dev/ssr-handler.mjs +++ b/packages/react-server/lib/dev/ssr-handler.mjs @@ -24,6 +24,8 @@ import { MAIN_MODULE, MEMORY_CACHE_CONTEXT, MODULE_LOADER, + OTEL_SPAN, + OTEL_CONTEXT, REDIRECT_CONTEXT, RENDER, RENDER_CONTEXT, @@ -52,7 +54,7 @@ export default async function ssrHandler(root) { const renderStream = createWorker(); const moduleCacheStorage = new AsyncLocalStorage(); - return async (httpContext) => { + return async function ssr(httpContext) { return new Promise((resolve, reject) => { try { const noCache = @@ -81,6 +83,9 @@ export default async function ssrHandler(root) { [COLLECT_STYLESHEETS]: collectStylesheets, [ACTION_CONTEXT]: {}, [RENDER_STREAM]: renderStream, + // Propagate OTel span from the HTTP layer + [OTEL_SPAN]: httpContext._otelSpan ?? null, + [OTEL_CONTEXT]: httpContext._otelCtx ?? null, }, async () => { try { @@ -180,7 +185,8 @@ export default async function ssrHandler(root) { }; context$(RENDER_HANDLER, handler); - return resolve(await handler()); + const result = await handler(); + return resolve(result); } catch (e) { logger.error(e); return errorHandler(e).then(resolve, reject); diff --git a/packages/react-server/lib/handlers/not-found.mjs b/packages/react-server/lib/handlers/not-found.mjs index 881279b4..230764ef 100644 --- a/packages/react-server/lib/handlers/not-found.mjs +++ b/packages/react-server/lib/handlers/not-found.mjs @@ -1,5 +1,5 @@ export default async function notFoundHandler() { - return async () => { + return async function notFound() { return new Response("Not Found", { status: 404 }); }; } diff --git a/packages/react-server/lib/handlers/static.mjs b/packages/react-server/lib/handlers/static.mjs index 634f5540..0a2aa229 100644 --- a/packages/react-server/lib/handlers/static.mjs +++ b/packages/react-server/lib/handlers/static.mjs @@ -45,7 +45,7 @@ export default async function staticHandler(dir, options = {}) { const fileCache = new Map(); - return async (context) => { + return async function serveStatic(context) { if (context.request.method !== "GET") { return; } diff --git a/packages/react-server/lib/handlers/trailing-slash.mjs b/packages/react-server/lib/handlers/trailing-slash.mjs index c3f9c9a8..fa11324e 100644 --- a/packages/react-server/lib/handlers/trailing-slash.mjs +++ b/packages/react-server/lib/handlers/trailing-slash.mjs @@ -1,5 +1,8 @@ export default async function trailingSlash() { - return async ({ url: { pathname }, request: { method } }) => { + return async function trailingSlashRedirect({ + url: { pathname }, + request: { method }, + }) { if ( pathname !== "/" && pathname.endsWith("/") && diff --git a/packages/react-server/lib/http/middleware.mjs b/packages/react-server/lib/http/middleware.mjs index c16fb390..428ac39a 100644 --- a/packages/react-server/lib/http/middleware.mjs +++ b/packages/react-server/lib/http/middleware.mjs @@ -7,6 +7,11 @@ import { compose } from "./middlewares/compose.mjs"; import { ContextStorage } from "../../server/context.mjs"; import { getRuntime } from "../../server/runtime.mjs"; import { AFTER_CONTEXT, LOGGER_CONTEXT } from "../../server/symbols.mjs"; +import { + getMetrics, + startRequestSpan, + injectTraceContext, +} from "../../server/telemetry.mjs"; export function createContext( request, @@ -46,6 +51,8 @@ export function createMiddleware(handler, options = {}) { const { origin, trustProxy = false, defaultNotFound = false } = options; const run = normalizeHandler(handler); return async function nodeAdapter(req, res, next) { + let ctx; + let metrics; try { const headersObj = req.headers || {}; const xfProto = headerFirst(headersObj["x-forwarded-proto"]); @@ -94,7 +101,7 @@ export function createMiddleware(handler, options = {}) { const request = new Request(fullUrl, requestInit); const abortController = new AbortController(); const { signal } = abortController; - const ctx = createContext(request, { + ctx = createContext(request, { origin, runtime: "node", signal, @@ -111,6 +118,28 @@ export function createMiddleware(handler, options = {}) { ctx.ip = ip; ctx.host = host; ctx.protocol = protocol; + + // ── Telemetry: start root HTTP span ── + const requestStart = performance.now(); + metrics = getMetrics(); + metrics?.httpActiveRequests.add(1, { "http.method": req.method }); + + const { span: rootSpan, otelCtx } = await startRequestSpan( + `HTTP Request`, + headersObj, + { + "http.method": req.method, + "http.url": fullUrl, + "http.target": req.url, + "http.host": host, + "http.scheme": protocol, + "http.user_agent": headersObj["user-agent"] || "", + "net.peer.ip": ip || "", + } + ); + ctx._otelSpan = rootSpan; + ctx._otelCtx = otelCtx; + let response = await run(ctx); if (!response) { if (defaultNotFound && !next) @@ -123,6 +152,15 @@ export function createMiddleware(handler, options = {}) { if (ctx._setCookies?.length) for (const c of ctx._setCookies) response.headers.append("set-cookie", c); + + // ── Telemetry: record response attributes ── + rootSpan.setAttribute("http.status_code", response.status); + rootSpan.setAttribute( + "http.response_content_type", + response.headers.get("content-type") || "" + ); + await injectTraceContext(response.headers); + res.statusCode = response.status; for (const [k, v] of response.headers.entries()) res.setHeader(k, v); if (req.method === "HEAD" || !response.body) { @@ -176,6 +214,16 @@ export function createMiddleware(handler, options = {}) { req.off("aborted", onDisconnect); } + // ── Telemetry: finish root span and record metrics ── + const duration = performance.now() - requestStart; + rootSpan.end(); + metrics?.httpActiveRequests.add(-1, { "http.method": req.method }); + metrics?.httpRequestDuration.record(duration, { + "http.method": req.method, + "http.status_code": res.statusCode, + "http.route": req.url, + }); + try { const { afterHooks } = ctx; if (afterHooks) { @@ -194,6 +242,20 @@ export function createMiddleware(handler, options = {}) { logger.error(e); } } catch (e) { + // ── Telemetry: record error on root span ── + if (ctx?._otelSpan) { + try { + ctx._otelSpan.setStatus({ + code: 2 /* SpanStatusCode.ERROR */, + message: e?.message, + }); + ctx._otelSpan.recordException(e); + ctx._otelSpan.end(); + metrics?.httpActiveRequests.add(-1, { "http.method": req.method }); + } catch { + // no-op if OTel not available + } + } if (e.name !== "AbortError" && e.message !== "aborted") { if (next) next(e); else internalError(res, e); diff --git a/packages/react-server/lib/http/middlewares/compose.mjs b/packages/react-server/lib/http/middlewares/compose.mjs index 57b4b4a3..3dfe5ce9 100644 --- a/packages/react-server/lib/http/middlewares/compose.mjs +++ b/packages/react-server/lib/http/middlewares/compose.mjs @@ -1,3 +1,28 @@ +import { + getTracer, + getOtelContext, + makeSpanContext, +} from "../../../server/telemetry.mjs"; + +// ── Human-readable middleware display names ── +const MIDDLEWARE_DISPLAY_NAMES = { + devTooling: "Dev Tooling", + basePathStrip: "Base Path", + prerenderInit: "Prerender Init", + serveStatic: "Static Files", + trailingSlashRedirect: "Trailing Slash", + notFound: "Not Found Handler", + corsMiddleware: "CORS", + cookieMiddleware: "Cookies", + ssr: "SSR Handler", + composed: "Middleware Chain", +}; + +function middlewareDisplayName(fn, index) { + const raw = fn.name || `anonymous#${index}`; + return MIDDLEWARE_DISPLAY_NAMES[raw] ?? raw; +} + export function compose(middlewares) { if (!Array.isArray(middlewares)) throw new TypeError("middlewares must be an array"); @@ -15,9 +40,58 @@ export function compose(middlewares) { const fn = middlewares[i]; if (!fn) return undefined; context.next = () => dispatch(i + 1); - const result = await fn(context); - if (result === undefined) return context.next(); - return result; + + // ── Telemetry: per-middleware span ── + const tracer = getTracer(); + const parentCtx = context._otelCtx ?? getOtelContext(); + const displayName = middlewareDisplayName(fn, i); + const span = tracer.startSpan( + `Middleware: ${displayName}`, + { + attributes: { + "react_server.middleware.index": i, + "react_server.middleware.name": fn.name || `anonymous#${i}`, + "react_server.middleware.display_name": displayName, + }, + }, + parentCtx ?? undefined + ); + // Propagate middleware span as parent context so inner spans + // (e.g. RSC/SSR Render) nest under this middleware + const prevOtelCtx = context._otelCtx; + context._otelCtx = makeSpanContext(span, parentCtx); + + // Wrap next() so the current middleware span ends before dispatching + // the next middleware. This ensures each span only measures its own work. + const originalNext = context.next; + let spanEnded = false; + context.next = () => { + if (!spanEnded) { + span.end(); + spanEnded = true; + } + context._otelCtx = prevOtelCtx; + return originalNext(); + }; + try { + const result = await fn(context); + // If middleware returned without calling next(), end span here + if (!spanEnded) { + span.end(); + spanEnded = true; + } + context._otelCtx = prevOtelCtx; + if (result === undefined) return context.next(); + return result; + } catch (error) { + span.recordException(error); + if (!spanEnded) { + span.end(); + spanEnded = true; + } + context._otelCtx = prevOtelCtx; + throw error; + } } return dispatch(0); }; diff --git a/packages/react-server/lib/plugins/import-remote.mjs b/packages/react-server/lib/plugins/import-remote.mjs index beb24748..ce7cd879 100644 --- a/packages/react-server/lib/plugins/import-remote.mjs +++ b/packages/react-server/lib/plugins/import-remote.mjs @@ -38,6 +38,10 @@ export default function(props) { }, async handler(code, id) { try { + if (!/from\s+["']https?:\/\//.test(code)) { + return null; + } + const ast = await parse(code, id); let hasRemoteImport = false; diff --git a/packages/react-server/lib/plugins/optimize-deps.mjs b/packages/react-server/lib/plugins/optimize-deps.mjs index f7d5b382..241d9c73 100644 --- a/packages/react-server/lib/plugins/optimize-deps.mjs +++ b/packages/react-server/lib/plugins/optimize-deps.mjs @@ -8,10 +8,9 @@ import { bareImportRE, hasClientComponents, hasClientComponentsAsync, - isModule, + isESMSyntaxAsync, isRootModule, nodeResolve, - readFileCachedAsync, tryStat, } from "../utils/module.mjs"; @@ -149,6 +148,15 @@ export { findPackagesWithClientComponents }; export default function optimizeDeps() { let clientComponentPackages = null; + // Cache stat results to avoid repeated synchronous statSync syscalls + const statCache = new Map(); + function isFileCached(filePath) { + if (statCache.has(filePath)) return statCache.get(filePath); + const result = tryStat(filePath)?.isFile() ?? false; + statCache.set(filePath, result); + return result; + } + return { name: "react-server:optimize-deps", enforce: "pre", @@ -167,6 +175,9 @@ export default function optimizeDeps() { } }, async resolveId(specifier, importer, resolveOptions) { + // Skip virtual modules - they're handled by their respective plugins + if (specifier[0] === "\0") return null; + try { const resolved = await this.resolve(specifier, importer, { ...resolveOptions, @@ -177,7 +188,7 @@ export default function optimizeDeps() { (this.environment.name === "rsc" || this.environment.name === "ssr") && /\.[cm]?js$/.test(path) && - tryStat(path)?.isFile() + isFileCached(path) ) { // Check if this is a bare import (not relative/absolute) const isBareImport = @@ -189,25 +200,9 @@ export default function optimizeDeps() { return resolved; } - // Check if file is ESM by parsing it (more reliable than package.json type) - let fileIsESM = isModule(path); - if (!fileIsESM && path.endsWith(".js")) { - try { - const content = await readFileCachedAsync(path); - if (content) { - const ast = this.parse(content); - fileIsESM = ast.body.some( - (node) => - node.type === "ImportDeclaration" || - node.type === "ExportNamedDeclaration" || - node.type === "ExportDefaultDeclaration" || - node.type === "ExportAllDeclaration" - ); - } - } catch { - // If parsing fails, fall back to package.json type - } - } + // Check if file is ESM using cached extension + regex detection + // (.mjs/.cjs resolved by extension, .js by cached content regex scan) + const fileIsESM = await isESMSyntaxAsync(path); // If the resolved file is CJS, externalize it if (!fileIsESM) { @@ -249,7 +244,7 @@ export default function optimizeDeps() { (specifier[0] !== "." && specifier[0] !== "/")) && applyAlias(alias, specifier) === specifier && !isRootModule(path) && - tryStat(path)?.isFile() + isFileCached(path) ) { // Don't optimize packages with client components - optimization breaks // React Context because optimized bundles create separate context instances diff --git a/packages/react-server/lib/plugins/optional-deps.mjs b/packages/react-server/lib/plugins/optional-deps.mjs new file mode 100644 index 00000000..25d96728 --- /dev/null +++ b/packages/react-server/lib/plugins/optional-deps.mjs @@ -0,0 +1,62 @@ +import { createRequire } from "node:module"; + +const __require = createRequire(import.meta.url); + +const VIRTUAL_EMPTY = "\0virtual:optional-dep-empty"; + +/** + * Vite/Rolldown plugin that resolves optional dependencies to an empty module + * when they aren't installed. This is essential for edge builds (e.g. + * Cloudflare Workers) where there's no node_modules at runtime. + * + * Behaviour: + * - Package matches `forceEmpty` → always resolve to empty module + * - Package installed → let the bundler resolve & bundle it normally + * - Package NOT installed → resolve to a virtual empty module (noop) + * + * @param {RegExp[]} patterns - Array of regexes matching package specifiers + * @param {{ forceEmpty?: RegExp[] }} [opts] - Additional options + * @param {RegExp[]} [opts.forceEmpty] - Patterns to always resolve to empty, + * even if the package is installed (e.g. @opentelemetry/* when telemetry is disabled) + */ +export default function optionalDeps(patterns, { forceEmpty = [] } = {}) { + const resolved = new Map(); + + function isOptional(id) { + return patterns.some((re) => re.test(id)); + } + + function isForceEmpty(id) { + return forceEmpty.some((re) => re.test(id)); + } + + function canResolve(id) { + if (resolved.has(id)) return resolved.get(id); + try { + __require.resolve(id); + resolved.set(id, true); + return true; + } catch { + resolved.set(id, false); + return false; + } + } + + return { + name: "react-server:optional-deps", + enforce: "pre", + resolveId(id) { + if (isOptional(id) && (isForceEmpty(id) || !canResolve(id))) { + return VIRTUAL_EMPTY; + } + // let the default resolver handle it + return null; + }, + load(id) { + if (id === VIRTUAL_EMPTY) { + return "export default {};"; + } + return null; + }, + }; +} diff --git a/packages/react-server/lib/plugins/telemetry-hooks.mjs b/packages/react-server/lib/plugins/telemetry-hooks.mjs new file mode 100644 index 00000000..da9795f9 --- /dev/null +++ b/packages/react-server/lib/plugins/telemetry-hooks.mjs @@ -0,0 +1,145 @@ +/** + * Vite plugin that instruments all other plugins' hooks with OpenTelemetry spans. + * + * When telemetry is enabled, this plugin wraps the `resolveId`, `load`, + * `transform`, `buildStart`, `buildEnd`, `configResolved`, `configureServer`, + * and `handleHotUpdate` hooks on every plugin so each invocation is individually + * traced. + * + * This gives full visibility into how long each Vite plugin hook takes — even + * built-in plugins. + */ + +const TRACED_HOOKS = [ + "resolveId", + "load", + "transform", + "buildStart", + "buildEnd", + "handleHotUpdate", +]; + +/** + * Wraps a single hook function to emit an OTel span on each call. + */ +function wrapHook( + hookName, + pluginName, + originalHook, + getTracer, + getOtelContext +) { + // Hooks can be either a function or an object { handler, order, ... } + if ( + typeof originalHook === "object" && + originalHook !== null && + originalHook.handler + ) { + return { + ...originalHook, + handler: wrapHookFn( + hookName, + pluginName, + originalHook.handler, + getTracer, + getOtelContext + ), + }; + } + if (typeof originalHook === "function") { + return wrapHookFn( + hookName, + pluginName, + originalHook, + getTracer, + getOtelContext + ); + } + return originalHook; +} + +function wrapHookFn(hookName, pluginName, fn, getTracer, getOtelContext) { + const wrapped = async function (...args) { + const tracer = getTracer(); + // Check if it's the noop tracer (has no actual recording) + if (!tracer || !tracer.startSpan) return fn.apply(this, args); + + const parentCtx = getOtelContext?.() ?? undefined; + const span = tracer.startSpan( + `Vite plugin [${pluginName}].${hookName}: `, + { + attributes: { + "react_server.vite.plugin": pluginName, + "react_server.vite.hook": hookName, + ...(hookName === "resolveId" && args[0] + ? { "react_server.vite.module_id": String(args[0]).slice(0, 200) } + : {}), + ...(hookName === "load" && args[0] + ? { "react_server.vite.module_id": String(args[0]).slice(0, 200) } + : {}), + ...(hookName === "transform" && args[1] + ? { "react_server.vite.module_id": String(args[1]).slice(0, 200) } + : {}), + }, + }, + parentCtx + ); + + try { + const result = await fn.apply(this, args); + span.end(); + return result; + } catch (error) { + span.setStatus({ code: 2, message: error?.message }); + span.recordException(error); + span.end(); + throw error; + } + }; + + // Preserve function name for debugging + Object.defineProperty(wrapped, "name", { value: fn.name || hookName }); + return wrapped; +} + +/** + * Creates a Vite plugin that instruments all other plugins' hooks. + * + * Must be added as the LAST plugin so that `configResolved` sees all plugins. + * Only added when telemetry is enabled (tracer already initialized). + */ +export default function telemetryHooks() { + let telemetryMod = null; + const telemetryReady = import("../../server/telemetry.mjs").then((mod) => { + telemetryMod = mod; + }); + + return { + name: "react-server:telemetry-hooks", + enforce: "pre", + async configResolved(resolvedConfig) { + // Wait for the telemetry module to be loaded + await telemetryReady; + if (!telemetryMod) return; + + const { getTracer, getOtelContext } = telemetryMod; + + // Wrap hooks on all other plugins + for (const plugin of resolvedConfig.plugins) { + if (plugin.name === "react-server:telemetry-hooks") continue; + + for (const hookName of TRACED_HOOKS) { + if (plugin[hookName]) { + plugin[hookName] = wrapHook( + hookName, + plugin.name, + plugin[hookName], + getTracer, + getOtelContext + ); + } + } + } + }, + }; +} diff --git a/packages/react-server/lib/start/create-server.mjs b/packages/react-server/lib/start/create-server.mjs index 67934ed7..4d6d4257 100644 --- a/packages/react-server/lib/start/create-server.mjs +++ b/packages/react-server/lib/start/create-server.mjs @@ -22,6 +22,12 @@ import { MEMORY_CACHE_CONTEXT, WORKER_THREAD, } from "../../server/symbols.mjs"; +import { + resolveTelemetryConfig, + initTelemetry, + shutdownTelemetry, + getTracer, +} from "../../server/telemetry.mjs"; import notFoundHandler from "../handlers/not-found.mjs"; import staticHandler from "../handlers/static.mjs"; import trailingSlashHandler from "../handlers/trailing-slash.mjs"; @@ -52,6 +58,19 @@ export default async function createServer(root, options) { const config = getRuntime(CONFIG_CONTEXT)?.[CONFIG_ROOT] ?? {}; + // ── Telemetry: initialize OpenTelemetry SDK ── + const telemetryConfig = resolveTelemetryConfig(config); + await initTelemetry(telemetryConfig); + + // ── Telemetry: server startup span ── + const startupTracer = getTracer(); + const startupSpan = startupTracer.startSpan("Server Startup", { + attributes: { + "react_server.mode": "production", + "react_server.root": root || "file-router", + }, + }); + const initialRuntime = { [MEMORY_CACHE_CONTEXT]: new StorageCache(memoryDriver), }; @@ -67,7 +86,9 @@ export default async function createServer(root, options) { const publicDir = typeof config.public === "string" ? config.public : "public"; const initialHandlers = await Promise.all([ - async () => PrerenderStorage.enterWith({}), + async function prerenderInit() { + PrerenderStorage.enterWith({}); + }, staticHandler(join(cwd, options.outDir, "dist"), { cwd: join(options.outDir, "dist"), }), @@ -90,7 +111,7 @@ export default async function createServer(root, options) { notFoundHandler(), ]); if (config.base) { - initialHandlers.unshift(async (context) => { + initialHandlers.unshift(async function basePathStrip(context) { if (context.url.pathname.startsWith(config.base)) { context.url.pathname = context.url.pathname.slice(config.base.length) || "/"; @@ -193,5 +214,15 @@ export default async function createServer(root, options) { }); } + // ── Telemetry: flush on server close ── + if (httpServer) { + httpServer.on("close", () => { + shutdownTelemetry(); + }); + } + + // ── Telemetry: end startup span ── + startupSpan.end(); + return server; } diff --git a/packages/react-server/lib/start/edge.mjs b/packages/react-server/lib/start/edge.mjs index 7378104e..9c415cf0 100644 --- a/packages/react-server/lib/start/edge.mjs +++ b/packages/react-server/lib/start/edge.mjs @@ -8,6 +8,10 @@ import { MEMORY_CACHE_CONTEXT, WORKER_THREAD, } from "../../server/symbols.mjs"; +import { + resolveTelemetryConfig, + initEdgeTelemetry, +} from "../../server/telemetry.mjs"; import notFoundHandler from "../handlers/not-found.mjs"; import trailingSlashHandler from "../handlers/trailing-slash.mjs"; import { getServerCors } from "../utils/server-config.mjs"; @@ -32,6 +36,12 @@ export function reactServer(root, options = {}, initialConfig = {}) { runtime$(CONFIG_CONTEXT, config); await createLogger({ logger: console, ...config[CONFIG_ROOT] }); + // ── Telemetry: initialize for edge runtime ── + const telemetryConfig = resolveTelemetryConfig( + config?.[CONFIG_ROOT] ?? {} + ); + await initEdgeTelemetry(telemetryConfig); + if (!options.outDir) { options.outDir = ".react-server"; } diff --git a/packages/react-server/lib/start/ssr-handler.mjs b/packages/react-server/lib/start/ssr-handler.mjs index 7ce82203..bcfef6cd 100644 --- a/packages/react-server/lib/start/ssr-handler.mjs +++ b/packages/react-server/lib/start/ssr-handler.mjs @@ -32,6 +32,8 @@ import { MEMORY_CACHE_CONTEXT, MODULE_LOADER, MODULE_CACHE, + OTEL_SPAN, + OTEL_CONTEXT, POSTPONE_CONTEXT, POSTPONE_STATE, PRELUDE_HTML, @@ -229,7 +231,7 @@ export default async function ssrHandler(root, options = {}) { }); }; - return async (httpContext) => { + return async function ssr(httpContext) { const noCache = httpContext.request.headers.get("cache-control") === "no-cache"; @@ -261,6 +263,9 @@ export default async function ssrHandler(root, options = {}) { [ERROR_BOUNDARY]: ErrorBoundary, [MODULE_CACHE]: moduleCacheStorage, [LINK_QUEUE]: linkQueueStorage, + // Propagate OTel span from the HTTP layer into per‑request context + [OTEL_SPAN]: httpContext._otelSpan ?? null, + [OTEL_CONTEXT]: httpContext._otelCtx ?? null, }, async () => { if (!noCache) { @@ -362,7 +367,14 @@ export default async function ssrHandler(root, options = {}) { if (getContext(POSTPONE_CONTEXT) === null) { context$(POSTPONE_CONTEXT, true); } - render(Component, {}, { middlewareError }).then(resolve, reject); + render(Component, {}, { middlewareError }).then( + (result) => { + resolve(result); + }, + (err) => { + reject(err); + } + ); } ); }); diff --git a/packages/react-server/package.json b/packages/react-server/package.json index 7240d7fe..05c41625 100644 --- a/packages/react-server/package.json +++ b/packages/react-server/package.json @@ -163,6 +163,10 @@ "types": "./worker/index.d.ts", "default": "./worker/index.mjs" }, + "./telemetry": { + "types": "./telemetry/index.d.ts", + "default": "./telemetry/index.mjs" + }, "./cache/*": "./cache/*", "./client/*": "./client/*", "./server/*": "./server/*", @@ -246,6 +250,46 @@ "devDependencies": { "@types/node": "^20.10.0" }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/core": "^1.0.0 || ^2.0.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.50.0 || ^1.0.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.50.0 || ^1.0.0", + "@opentelemetry/resources": "^1.0.0 || ^2.0.0", + "@opentelemetry/sdk-metrics": "^1.0.0 || ^2.0.0", + "@opentelemetry/sdk-node": "^0.50.0 || ^1.0.0", + "@opentelemetry/sdk-trace-base": "^1.0.0 || ^2.0.0", + "@opentelemetry/semantic-conventions": "^1.0.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/sdk-node": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-http": { + "optional": true + }, + "@opentelemetry/exporter-metrics-otlp-http": { + "optional": true + }, + "@opentelemetry/sdk-metrics": { + "optional": true + }, + "@opentelemetry/core": { + "optional": true + }, + "@opentelemetry/resources": { + "optional": true + }, + "@opentelemetry/semantic-conventions": { + "optional": true + } + }, "engines": { "node": ">=20.10.0" } diff --git a/packages/react-server/server/dev-trace-exporter.mjs b/packages/react-server/server/dev-trace-exporter.mjs new file mode 100644 index 00000000..d4fc802a --- /dev/null +++ b/packages/react-server/server/dev-trace-exporter.mjs @@ -0,0 +1,419 @@ +/** + * A pretty-printed console span exporter for development. + * + * Buffers spans per trace and renders a compact tree view with: + * - Request summary header (method, path, status, total duration) + * - Hierarchical tree using parent-child relationships + * - Color-coded durations (green / yellow / red) + * - Proportional timing bars + * - Key attributes displayed inline + * + * Used automatically in dev mode when telemetry is enabled. + */ + +import colors from "picocolors"; + +// ── Thresholds & layout ─────────────────────────────────────────────── + +const SLOW_MS = 100; +const WARN_MS = 20; +const BAR_WIDTH = 12; +const MIN_DURATION_MS = 1; // Hide spans shorter than this from dev console + +// ── Helpers ─────────────────────────────────────────────────────────── + +function hrTimeToMs(hrTime) { + if (Array.isArray(hrTime)) return hrTime[0] * 1000 + hrTime[1] / 1e6; + return 0; +} + +function formatDuration(ms) { + if (ms < 0.01) return colors.dim("—"); + if (ms < 1) return colorDuration(`${(ms * 1000).toFixed(0)}µs`, ms); + if (ms < 1000) return colorDuration(`${ms.toFixed(1)}ms`, ms); + return colorDuration(`${(ms / 1000).toFixed(2)}s`, ms); +} + +function colorDuration(text, ms) { + if (ms >= SLOW_MS) return colors.red(text); + if (ms >= WARN_MS) return colors.yellow(text); + return colors.green(text); +} + +function timingBar(ms, maxMs) { + if (maxMs <= 0 || ms <= 0) return ""; + const filled = Math.max(1, Math.round((ms / maxMs) * BAR_WIDTH)); + const bar = "░".repeat(Math.min(filled, BAR_WIDTH)); + if (ms >= SLOW_MS) return colors.red(bar); + if (ms >= WARN_MS) return colors.yellow(bar); + return colors.dim(bar); +} + +function statusBadge(code) { + if (code >= 500) return colors.red(colors.bold(String(code))); + if (code >= 400) return colors.yellow(String(code)); + if (code >= 300) return colors.cyan(String(code)); + if (code >= 200) return colors.green(String(code)); + return colors.dim(String(code)); +} + +function methodColor(method) { + switch (method) { + case "GET": + return colors.cyan; + case "POST": + return colors.green; + case "PUT": + return colors.yellow; + case "DELETE": + return colors.red; + case "PATCH": + return colors.magenta; + default: + return colors.white; + } +} + +/** Pick the most useful inline detail from span attributes. */ +function inlineDetail(name, attrs) { + const parts = []; + // Cache operations: show hit/miss status + if (attrs["react_server.cache.hit"] !== undefined) { + parts.push( + attrs["react_server.cache.hit"] + ? colors.green("HIT") + : colors.yellow("MISS") + ); + if ( + attrs["react_server.cache.provider"] && + attrs["react_server.cache.provider"] !== "default" + ) + parts.push(colors.dim(`(${attrs["react_server.cache.provider"]})`)); + } + // Render type + if (attrs["react_server.render_type"]) + parts.push(colors.magenta(attrs["react_server.render_type"])); + // Server functions: show function ID + if (attrs["react_server.server_function.id"]) + parts.push(colors.yellow(attrs["react_server.server_function.id"])); + // Vite plugin hooks: show module being processed + if (attrs["react_server.vite.module_id"]) { + let modId = attrs["react_server.vite.module_id"]; + // Shorten long module paths + if (modId.length > 60) modId = "…" + modId.slice(-55); + parts.push(colors.dim(modId)); + } + return parts.length ? " " + parts.join(" ") : ""; +} + +// ── Tree construction & rendering ───────────────────────────────────── + +function buildTree(spans) { + const childrenOf = new Map(); + + for (const span of spans) { + childrenOf.set(span.spanContext().spanId, []); + } + + const roots = []; + for (const span of spans) { + const parentId = span.parentSpanContext?.spanId; + if (parentId && childrenOf.has(parentId)) { + childrenOf.get(parentId).push(span); + } else { + roots.push(span); + } + } + + for (const children of childrenOf.values()) { + children.sort((a, b) => hrTimeToMs(a.startTime) - hrTimeToMs(b.startTime)); + } + + return { roots, childrenOf }; +} + +function isVitePluginSpan(span) { + return span.name.startsWith("Vite plugin ["); +} + +/** + * Group green-duration Vite plugin spans by name. + * Yellow/red/error spans are kept individual. Non-Vite spans pass through. + * + * Returns a flat list of "render items" in original order: + * { type: "span", span } — individual span + * { type: "group", name, count, totalMs } — collapsed group + */ +function groupViteSpans(spans, childrenOf) { + // First pass: bucket green Vite spans by name + const greenBuckets = new Map(); // name → span[] + const items = []; // ordered render items (placeholder for groups) + + for (const span of spans) { + const durationMs = hrTimeToMs(span.endTime) - hrTimeToMs(span.startTime); + const isVite = isVitePluginSpan(span); + const isGreen = durationMs < WARN_MS; + const hasError = span.status?.code === 2; + const hasChildren = + (childrenOf.get(span.spanContext().spanId) || []).length > 0; + + if (isVite && isGreen && !hasError && !hasChildren) { + if (!greenBuckets.has(span.name)) { + greenBuckets.set(span.name, []); + // Insert a group placeholder at position of first occurrence + items.push({ type: "group", name: span.name, _bucket: span.name }); + } + greenBuckets.get(span.name).push(span); + } else { + items.push({ type: "span", span }); + } + } + + // Second pass: resolve group placeholders + const result = []; + for (const item of items) { + if (item.type === "group") { + const bucket = greenBuckets.get(item._bucket); + if (!bucket || bucket.length === 0) continue; + const totalMs = bucket.reduce( + (sum, s) => sum + (hrTimeToMs(s.endTime) - hrTimeToMs(s.startTime)), + 0 + ); + result.push({ + type: "group", + name: item.name, + count: bucket.length, + totalMs, + }); + } else { + result.push(item); + } + } + return result; +} + +function renderTree(spans, childrenOf, maxMs, prefix) { + const lines = []; + + // Separate spans by visibility: >= MIN_DURATION_MS shown individually, rest collapsed + const visibleSpans = []; + let skippedCount = 0; + let skippedTotalMs = 0; + + for (const span of spans) { + const durationMs = hrTimeToMs(span.endTime) - hrTimeToMs(span.startTime); + if (durationMs < MIN_DURATION_MS) { + skippedCount++; + skippedTotalMs += durationMs; + // Also count all descendants as skipped + const countDescendants = (spanId) => { + const kids = childrenOf.get(spanId) || []; + for (const kid of kids) { + skippedCount++; + skippedTotalMs += hrTimeToMs(kid.endTime) - hrTimeToMs(kid.startTime); + countDescendants(kid.spanContext().spanId); + } + }; + countDescendants(span.spanContext().spanId); + } else { + visibleSpans.push(span); + } + } + + // Group green Vite plugin spans by name+hook + const renderItems = groupViteSpans(visibleSpans, childrenOf); + + for (let i = 0; i < renderItems.length; i++) { + const item = renderItems[i]; + const isLast = i === renderItems.length - 1 && skippedCount === 0; + const connector = isLast ? "└─" : "├─"; + const childPrefix = isLast ? " " : "│ "; + + if (item.type === "group") { + // Collapsed Vite plugin group + const bar = timingBar(item.totalMs, maxMs); + const dur = formatDuration(item.totalMs); + const barStr = bar ? " " + bar : ""; + const countStr = colors.dim(` ×${item.count}`); + + lines.push( + `${colors.dim(prefix + connector)} ${colors.white(item.name)}${countStr}${barStr} ${dur}` + ); + continue; + } + + // Individual span + const span = item.span; + const children = childrenOf.get(span.spanContext().spanId) || []; + const durationMs = hrTimeToMs(span.endTime) - hrTimeToMs(span.startTime); + const name = span.name; + const detail = inlineDetail(name, span.attributes || {}); + + const hasError = span.status?.code === 2; + const nameStr = hasError ? colors.red(name) : colors.white(name); + const errorSuffix = + hasError && span.status?.message + ? " " + colors.red(colors.dim(span.status.message)) + : ""; + + const bar = timingBar(durationMs, maxMs); + const dur = formatDuration(durationMs); + const barStr = bar ? " " + bar : ""; + + lines.push( + `${colors.dim(prefix + connector)} ${nameStr}${detail}${errorSuffix}${barStr} ${dur}` + ); + + if (children.length > 0) { + lines.push( + ...renderTree(children, childrenOf, maxMs, prefix + childPrefix) + ); + } + } + + // Summary line for all collapsed (sub-threshold) spans + if (skippedCount > 0) { + const connector = "└─"; + const summary = colors.dim( + `${skippedCount} span${skippedCount > 1 ? "s" : ""} (<${MIN_DURATION_MS}ms)` + ); + lines.push(`${colors.dim(prefix + connector)} ${summary}`); + } + + return lines; +} + +// ── Exporter ────────────────────────────────────────────────────────── + +/** + * Dev console span exporter. + * + * Buffers spans by traceId for a short window, then renders the full + * trace as a tree. This is needed because SimpleSpanProcessor exports + * one span at a time as each span ends. + */ +export class DevConsoleSpanExporter { + constructor(options = {}) { + this._stopped = false; + this._buffer = new Map(); // traceId → { spans[], timer, hasRoot } + this._maxWait = options.maxWait ?? 5000; + } + + export(spans, resultCallback) { + if (this._stopped) { + resultCallback({ code: 1 }); + return; + } + + for (const span of spans) { + const traceId = span.spanContext().traceId; + if (!this._buffer.has(traceId)) { + this._buffer.set(traceId, { spans: [], timer: null, hasRoot: false }); + } + const entry = this._buffer.get(traceId); + entry.spans.push(span); + + // A span without a parent is the root — the trace is complete + if (!span.parentSpanContext?.spanId) { + entry.hasRoot = true; + } + + if (entry.hasRoot) { + // Root arrived — flush immediately + if (entry.timer) clearTimeout(entry.timer); + // Use a microtask so any remaining spans from the same tick arrive first + entry.timer = setTimeout(() => this._flush(traceId), 5); + } else if (!entry.timer) { + // Safety timeout — flush even if root never arrives + entry.timer = setTimeout(() => this._flush(traceId), this._maxWait); + } + } + + resultCallback({ code: 0 }); + } + + _flush(traceId) { + const entry = this._buffer.get(traceId); + if (!entry) return; + this._buffer.delete(traceId); + this._render(entry.spans); + } + + _render(spans) { + if (spans.length === 0) return; + + const { roots, childrenOf } = buildTree(spans); + + for (const root of roots) { + const rootId = root.spanContext().spanId; + const rootDuration = + hrTimeToMs(root.endTime) - hrTimeToMs(root.startTime); + const children = childrenOf.get(rootId) || []; + const attrs = root.attributes || {}; + + // Skip standalone Vite spans (module resolution, transforms, etc.) + // They are only useful as children of an HTTP request trace. + if (root.name.startsWith("Vite ")) { + continue; + } + + // Skip other non-HTTP root spans that are below threshold + if ( + !root.name.startsWith("HTTP") && + root.name !== "HTTP Request" && + rootDuration < MIN_DURATION_MS + ) { + continue; + } + + // ── Header line ── + if (root.name.startsWith("HTTP") || root.name === "HTTP Request") { + const method = attrs["http.method"] || "???"; + const target = attrs["http.target"] || attrs["http.url"] || "/"; + const statusCode = attrs["http.status_code"]; + const colorFn = methodColor(method); + + const parts = [ + colorFn(colors.bold(method)), + colors.white(target), + statusCode != null ? statusBadge(statusCode) : null, + formatDuration(rootDuration), + ].filter(Boolean); + + console.log(`\n ${parts.join(" ")}`); + } else { + // Non-HTTP root span + console.log( + `\n ${colors.white(colors.bold(root.name))} ${formatDuration(rootDuration)}` + ); + } + + // ── Child tree ── + if (children.length > 0) { + const lines = renderTree(children, childrenOf, rootDuration, " "); + for (const line of lines) { + console.log(line); + } + } + } + } + + shutdown() { + for (const [, entry] of this._buffer) { + if (entry.timer) clearTimeout(entry.timer); + this._render(entry.spans); + } + this._buffer.clear(); + this._stopped = true; + return Promise.resolve(); + } + + forceFlush() { + for (const [, entry] of this._buffer) { + if (entry.timer) clearTimeout(entry.timer); + this._render(entry.spans); + } + this._buffer.clear(); + return Promise.resolve(); + } +} diff --git a/packages/react-server/server/render-rsc.jsx b/packages/react-server/server/render-rsc.jsx index 92923084..f4e7e26a 100644 --- a/packages/react-server/server/render-rsc.jsx +++ b/packages/react-server/server/render-rsc.jsx @@ -49,6 +49,12 @@ import { SERVER_FUNCTION_NOT_FOUND, } from "@lazarv/react-server/server/symbols.mjs"; import { ServerFunctionNotFoundError } from "./action-state.mjs"; +import { + getTracer, + getMetrics, + getOtelContext, + makeSpanContext, +} from "./telemetry.mjs"; import { cwd } from "../lib/sys.mjs"; import { clientReferenceMap } from "@lazarv/react-server/dist/server/client-reference-map"; import { serverReferenceMap as _serverReferenceMap } from "@lazarv/react-server/dist/server/server-reference-map"; @@ -213,7 +219,44 @@ export async function render(Component, props = {}, options = {}) { } } - const { data, actionId, error } = await action(); + const { data, actionId, error } = await (async () => { + // ── Telemetry: server function span ── + const tracer = getTracer(); + const parentCtx = getOtelContext(); + const actionSpan = tracer.startSpan( + "Server Function", + { + attributes: { + "react_server.server_function.id": + serverActionHeader || "form-action", + "react_server.server_function.is_form": !!isFormData, + }, + }, + parentCtx ?? undefined + ); + const actionStart = performance.now(); + try { + const result = await action(); + actionSpan.setAttribute( + "react_server.server_function.has_error", + !!result.error + ); + if (result.error) { + actionSpan.recordException(result.error); + } + return result; + } catch (e) { + actionSpan.recordException(e); + throw e; + } finally { + actionSpan.end(); + const metrics = getMetrics(); + metrics?.actionDuration.record(performance.now() - actionStart, { + "react_server.server_function.id": + serverActionHeader || "form-action", + }); + } + })(); if (error?.name === SERVER_FUNCTION_NOT_FOUND) { const e = new ServerFunctionNotFoundError(); @@ -505,6 +548,19 @@ export async function render(Component, props = {}, options = {}) { } let hasError = false; + const rscRenderTracer = getTracer(); + const rscRenderParentCtx = getOtelContext(); + const rscFlightSpan = rscRenderTracer.startSpan( + "Render", + { + attributes: { + "react_server.render_type": "RSC", + "react_server.outlet": outlet || "PAGE_ROOT", + "http.url": context.url?.href, + }, + }, + rscRenderParentCtx ?? undefined + ); const stream = new ReadableStream({ type: "bytes", async start(controller) { @@ -627,6 +683,15 @@ export async function render(Component, props = {}, options = {}) { ttl ); } + + // End RSC flight rendering span + if (hasError) { + rscFlightSpan.setStatus({ + code: 2, + message: "RSC rendering error", + }); + } + rscFlightSpan.end(); }, }); } else if (renderContext.flags.isHTML || renderContext.flags.isRemote) { @@ -673,6 +738,23 @@ export async function render(Component, props = {}, options = {}) { context$(HTTP_RESPONSE, responsePromise); let hasError = false; + + // ── Telemetry: RSC rendering span (wraps the full RSC→SSR pipeline) ── + const renderTracer = getTracer(); + const renderParentCtx = getOtelContext(); + + const rscSpan = renderTracer.startSpan( + "Render", + { + attributes: { + "react_server.render_type": "RSC", + "react_server.outlet": outlet || "PAGE_ROOT", + "http.url": context.url?.href, + }, + }, + renderParentCtx ?? undefined + ); + const flight = server.renderToReadableStream( app, clientReferenceMap({ remote: remote || remoteRSC, origin }), @@ -693,12 +775,27 @@ export async function render(Component, props = {}, options = {}) { } ); + // ── Telemetry: SSR rendering span (child of RSC, consumes flight stream → HTML) ── + const rscSpanCtx = makeSpanContext(rscSpan, renderParentCtx); + const ssrSpan = renderTracer.startSpan( + "Render", + { + attributes: { + "react_server.render_type": "SSR", + "react_server.outlet": outlet || "PAGE_ROOT", + "http.url": context.url?.href, + }, + }, + rscSpanCtx + ); + const contextStore = ContextStorage.getStore(); const { onPostponed, prerender } = context; const prelude = getContext(PRELUDE_HTML); const postponed = getContext(POSTPONE_STATE); const importMap = getContext(IMPORT_MAP); let isStarted = false; + const stream = await renderStream({ stream: flight, bootstrapModules: @@ -779,6 +876,8 @@ export async function render(Component, props = {}, options = {}) { ...httpStatus, headers, }); + ssrSpan.end(); + rscSpan.end(); resolveResponse(response); resolve(response); }); @@ -789,6 +888,12 @@ export async function render(Component, props = {}, options = {}) { } (logger ?? console).error(e); hasError = true; + ssrSpan.setStatus({ code: 2, message: e?.message }); + ssrSpan.recordException(e); + ssrSpan.end(); + rscSpan.setStatus({ code: 2, message: e?.message }); + rscSpan.recordException(e); + rscSpan.end(); if (!isStarted) { ContextStorage.run(contextStore, async () => { context$(HTTP_STATUS, { diff --git a/packages/react-server/server/symbols.mjs b/packages/react-server/server/symbols.mjs index 962cc06d..176e4440 100644 --- a/packages/react-server/server/symbols.mjs +++ b/packages/react-server/server/symbols.mjs @@ -60,3 +60,9 @@ export const CONSOLE_PROXY = Symbol.for("CONSOLE_PROXY"); export const EXEC_OPTIONS = Symbol.for("EXEC_OPTIONS"); export const ABORT_SIGNAL = Symbol.for("ABORT_SIGNAL"); export const AFTER_CONTEXT = Symbol.for("AFTER_CONTEXT"); +export const OTEL_API = Symbol.for("OTEL_API"); +export const OTEL_TRACER = Symbol.for("OTEL_TRACER"); +export const OTEL_METER = Symbol.for("OTEL_METER"); +export const OTEL_SPAN = Symbol.for("OTEL_SPAN"); +export const OTEL_CONTEXT = Symbol.for("OTEL_CONTEXT"); +export const OTEL_SDK = Symbol.for("OTEL_SDK"); diff --git a/packages/react-server/server/telemetry.mjs b/packages/react-server/server/telemetry.mjs new file mode 100644 index 00000000..36c39b91 --- /dev/null +++ b/packages/react-server/server/telemetry.mjs @@ -0,0 +1,608 @@ +/** + * Core OpenTelemetry integration for @lazarv/react-server. + * + * This module provides: + * - SDK initialization (`initTelemetry`) + * - Helpers to create / retrieve spans from ContextStorage + * - Built-in metrics (request duration, active requests, render timings) + * - Trace-context propagation (W3C TraceContext) + * + * All OTel dependencies are loaded lazily so that the runtime has zero + * overhead when telemetry is disabled (default in production unless configured). + */ + +import { getContext, context$ } from "./context.mjs"; +import { getRuntime, runtime$ } from "./runtime.mjs"; +import { + OTEL_API, + OTEL_TRACER, + OTEL_METER, + OTEL_SPAN, + OTEL_CONTEXT, + OTEL_SDK, + LOGGER_CONTEXT, +} from "./symbols.mjs"; + +// ─── Noop fallbacks ────────────────────────────────────────────────────────── + +const NOOP_SPAN = { + setAttribute() { + return this; + }, + setAttributes() { + return this; + }, + addEvent() { + return this; + }, + setStatus() { + return this; + }, + end() {}, + isRecording() { + return false; + }, + recordException() {}, + spanContext() { + return { traceId: "", spanId: "", traceFlags: 0 }; + }, + updateName() { + return this; + }, +}; + +const NOOP_TRACER = { + startSpan() { + return NOOP_SPAN; + }, + startActiveSpan(_name, ...args) { + const fn = args[args.length - 1]; + return fn(NOOP_SPAN); + }, +}; + +const NOOP_COUNTER = { + add() {}, +}; +const NOOP_HISTOGRAM = { + record() {}, +}; +const NOOP_UP_DOWN_COUNTER = { + add() {}, +}; +const NOOP_METER = { + createCounter() { + return NOOP_COUNTER; + }, + createHistogram() { + return NOOP_HISTOGRAM; + }, + createUpDownCounter() { + return NOOP_UP_DOWN_COUNTER; + }, + createObservableGauge() { + return { addCallback() {} }; + }, +}; + +// ─── Lazy OTel API access ──────────────────────────────────────────────────── + +let _api = null; + +function getApi() { + return _api ?? getRuntime(OTEL_API) ?? null; +} + +async function otelApi() { + if (!_api) { + _api = await import("@opentelemetry/api"); + } + return _api; +} + +// ─── Public helpers ────────────────────────────────────────────────────────── + +/** + * Returns the active tracer (or a no-op tracer when telemetry is disabled). + */ +export function getTracer() { + return getRuntime(OTEL_TRACER) ?? NOOP_TRACER; +} + +/** + * Returns the active meter (or a no-op meter when telemetry is disabled). + */ +export function getMeter() { + return getRuntime(OTEL_METER) ?? NOOP_METER; +} + +/** + * Get the current request span from the per-request ContextStorage. + * Returns a noop span when telemetry is off or called outside a request. + */ +export function getSpan() { + return getContext(OTEL_SPAN) ?? NOOP_SPAN; +} + +/** + * Get the OTel context for the current request. + */ +export function getOtelContext() { + return getContext(OTEL_CONTEXT) ?? null; +} + +/** + * Store a span (and optional OTel context) in the per-request ContextStorage. + */ +export function setSpan(span, otelCtx) { + context$(OTEL_SPAN, span); + if (otelCtx) { + context$(OTEL_CONTEXT, otelCtx); + } +} + +/** + * Create a new OTel context with the given span set as the active span. + * Use this to propagate parent context so child spans are properly nested. + * + * This function is **synchronous** to avoid introducing async boundaries + * in the render pipeline (which would break AsyncLocalStorage propagation + * and cache behavior). It relies on `_api` being populated eagerly by + * `initTelemetry()` / `initEdgeTelemetry()` before any request is handled. + */ +export function makeSpanContext(span, parentCtx) { + const api = getApi(); + if (!api || span === NOOP_SPAN) return undefined; + return api.trace.setSpan(parentCtx || api.ROOT_CONTEXT, span); +} + +/** + * Execute `fn` within a child span of the current request span. + * + * @param {string} name - Span name + * @param {Record} [attributes] - Initial span attributes + * @param {(span: any) => Promise} fn - Function to execute inside the span + * @returns {Promise} + * + * @example + * ```js + * import { withSpan } from "@lazarv/react-server/telemetry"; + * + * const result = await withSpan("db.query", { "db.system": "postgres" }, async (span) => { + * const rows = await db.query("SELECT ..."); + * span.setAttribute("db.rows", rows.length); + * return rows; + * }); + * ``` + */ +export async function withSpan(name, attributes, fn) { + if (typeof attributes === "function") { + fn = attributes; + attributes = {}; + } + + const tracer = getTracer(); + if (tracer === NOOP_TRACER) { + return fn(NOOP_SPAN); + } + + const parentOtelCtx = getOtelContext(); + const api = await otelApi(); + const ctx = parentOtelCtx ?? api.context.active(); + const span = tracer.startSpan(name, { attributes }, ctx); + + try { + const result = await api.context.with(api.trace.setSpan(ctx, span), () => + fn(span) + ); + span.setStatus({ code: api.SpanStatusCode.OK }); + return result; + } catch (error) { + span.setStatus({ + code: api.SpanStatusCode.ERROR, + message: error?.message, + }); + span.recordException(error); + throw error; + } finally { + span.end(); + } +} + +// ─── Built-in metrics ──────────────────────────────────────────────────────── + +let _metrics = null; + +/** + * Get (or lazily create) the built-in metrics instruments. + */ +export function getMetrics() { + if (_metrics) return _metrics; + const meter = getMeter(); + if (meter === NOOP_METER) return null; + + _metrics = { + httpRequestDuration: meter.createHistogram("http.server.request.duration", { + description: "Duration of HTTP requests", + unit: "ms", + }), + httpActiveRequests: meter.createUpDownCounter( + "http.server.active_requests", + { + description: "Number of active HTTP requests", + } + ), + rscRenderDuration: meter.createHistogram( + "react_server.rsc.render.duration", + { + description: "Duration of RSC rendering", + unit: "ms", + } + ), + domRenderDuration: meter.createHistogram( + "react_server.dom.render.duration", + { + description: "Duration of SSR DOM rendering", + unit: "ms", + } + ), + actionDuration: meter.createHistogram( + "react_server.server_function.duration", + { + description: "Duration of server function execution", + unit: "ms", + } + ), + cacheHits: meter.createCounter("react_server.cache.hits", { + description: "Number of cache hits", + }), + cacheMisses: meter.createCounter("react_server.cache.misses", { + description: "Number of cache misses", + }), + }; + return _metrics; +} + +// ─── SDK Initialization ────────────────────────────────────────────────────── + +/** + * Resolve the telemetry config. Returns null when telemetry should be disabled. + * + * Telemetry is enabled when: + * 1. `config.telemetry.enabled` is explicitly `true`, OR + * 2. The `OTEL_EXPORTER_OTLP_ENDPOINT` env var is set, OR + * 3. The `REACT_SERVER_TELEMETRY` env var is "true" + */ +export function resolveTelemetryConfig(config) { + const env = typeof process !== "undefined" ? process.env : {}; + const telemetryConfig = config?.telemetry ?? {}; + + // Explicit opt-out via env var (e.g. set in test runner config) + if (env.REACT_SERVER_TELEMETRY === "false") { + return null; + } + + const enabled = + telemetryConfig.enabled === true || + !!env.OTEL_EXPORTER_OTLP_ENDPOINT || + env.REACT_SERVER_TELEMETRY === "true"; + + if (!enabled) return null; + + const isDev = env.NODE_ENV === "development" || env.NODE_ENV === undefined; + + return { + serviceName: + telemetryConfig.serviceName ?? + env.OTEL_SERVICE_NAME ?? + config?.name ?? + "@lazarv/react-server", + endpoint: + telemetryConfig.endpoint ?? + env.OTEL_EXPORTER_OTLP_ENDPOINT ?? + "http://localhost:4318", + exporter: + telemetryConfig.exporter ?? + (env.OTEL_EXPORTER_OTLP_ENDPOINT + ? "otlp" + : isDev + ? "dev-console" + : "otlp"), + sampleRate: telemetryConfig.sampleRate ?? 1.0, + propagators: telemetryConfig.propagators ?? ["w3c"], + metrics: { + enabled: telemetryConfig.metrics?.enabled !== false, + interval: telemetryConfig.metrics?.interval ?? 30000, + }, + }; +} + +/** + * Initialize the OpenTelemetry SDK for Node.js runtime. + * Stores the tracer and meter in RuntimeContextStorage. + * + * @param {object} telemetryConfig - Resolved config from `resolveTelemetryConfig()` + * @returns {Promise} The SDK instance, or null if not initialized + */ +export async function initTelemetry(telemetryConfig) { + if (!telemetryConfig) return null; + + const logger = getRuntime(LOGGER_CONTEXT); + const isDevConsole = + telemetryConfig.exporter === "dev-console" || + telemetryConfig.exporter === "console"; + + try { + // For dev-console / console exporters, use BasicTracerProvider with + // SimpleSpanProcessor so spans are exported immediately (not batched). + // This avoids importing OTLP exporters that try to connect to a collector. + if (isDevConsole) { + const [ + api, + { BasicTracerProvider, SimpleSpanProcessor, ConsoleSpanExporter }, + { resourceFromAttributes }, + { ATTR_SERVICE_NAME }, + ] = await Promise.all([ + import("@opentelemetry/api"), + import("@opentelemetry/sdk-trace-base"), + import("@opentelemetry/resources"), + import("@opentelemetry/semantic-conventions"), + ]); + + // Cache the API module so synchronous helpers (makeSpanContext) work + _api = api; + runtime$(OTEL_API, api); + + const resource = resourceFromAttributes({ + [ATTR_SERVICE_NAME]: telemetryConfig.serviceName, + "react_server.runtime": "node", + }); + + let exporter; + if (telemetryConfig.exporter === "dev-console") { + const { DevConsoleSpanExporter } = + await import("./dev-trace-exporter.mjs"); + exporter = new DevConsoleSpanExporter(); + } else { + exporter = new ConsoleSpanExporter(); + } + + const provider = new BasicTracerProvider({ + resource, + spanProcessors: [new SimpleSpanProcessor(exporter)], + }); + api.trace.setGlobalTracerProvider(provider); + + const tracer = api.trace.getTracer("@lazarv/react-server", "0.0.0"); + + runtime$(OTEL_TRACER, tracer); + runtime$(OTEL_SDK, provider); + + const log = logger?.info?.bind(logger) ?? console.log; + log("[telemetry] OpenTelemetry initialized"); + log( + `[telemetry] service=${telemetryConfig.serviceName} exporter=${telemetryConfig.exporter}` + ); + + return provider; + } + + // For OTLP exporter, use the full NodeSDK with batch processing. + const [ + { NodeSDK }, + { OTLPTraceExporter }, + { OTLPMetricExporter }, + { PeriodicExportingMetricReader }, + api, + { W3CTraceContextPropagator }, + ] = await Promise.all([ + import("@opentelemetry/sdk-node"), + import("@opentelemetry/exporter-trace-otlp-http"), + import("@opentelemetry/exporter-metrics-otlp-http"), + import("@opentelemetry/sdk-metrics"), + import("@opentelemetry/api"), + import("@opentelemetry/core"), + ]); + + // Cache the API module so synchronous helpers (makeSpanContext) work + _api = api; + runtime$(OTEL_API, api); + + // Use NodeSDK's built-in serviceName option instead of creating a custom + // resource. This avoids cross-version Resource class incompatibilities + // that can arise when pnpm hoists different @opentelemetry/resources + // versions for the user project vs sdk-node's own dependency. + // Additional resource attributes are set via OTEL_RESOURCE_ATTRIBUTES. + process.env.OTEL_RESOURCE_ATTRIBUTES = [ + process.env.OTEL_RESOURCE_ATTRIBUTES, + "react_server.runtime=node", + ] + .filter(Boolean) + .join(","); + + const traceExporter = new OTLPTraceExporter({ + url: `${telemetryConfig.endpoint}/v1/traces`, + }); + + const sdkConfig = { + serviceName: telemetryConfig.serviceName, + traceExporter, + textMapPropagator: new W3CTraceContextPropagator(), + }; + + if (telemetryConfig.metrics.enabled) { + sdkConfig.metricReader = new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter({ + url: `${telemetryConfig.endpoint}/v1/metrics`, + }), + exportIntervalMillis: telemetryConfig.metrics.interval, + }); + } + + const sdk = new NodeSDK(sdkConfig); + sdk.start(); + + const tracer = api.trace.getTracer("@lazarv/react-server", "0.0.0"); + const meter = api.metrics.getMeter("@lazarv/react-server", "0.0.0"); + + runtime$(OTEL_TRACER, tracer); + runtime$(OTEL_METER, meter); + runtime$(OTEL_SDK, sdk); + + const log = logger?.info?.bind(logger) ?? console.log; + log("[telemetry] OpenTelemetry initialized"); + log( + `[telemetry] service=${telemetryConfig.serviceName} exporter=${telemetryConfig.exporter} endpoint=${telemetryConfig.endpoint}` + ); + + return sdk; + } catch (error) { + const msg = + `[telemetry] Failed to initialize OpenTelemetry: ${error.message}. ` + + "Install @opentelemetry/sdk-node and related packages to enable telemetry."; + if (logger?.warn) { + logger.warn(msg); + } else { + console.warn(msg); + } + return null; + } +} + +/** + * Initialize lightweight telemetry for edge runtimes. + * Uses only @opentelemetry/api with a simple span processor. + * + * @param {object} telemetryConfig - Resolved config from `resolveTelemetryConfig()` + * @returns {Promise} + */ +export async function initEdgeTelemetry(telemetryConfig) { + if (!telemetryConfig) return null; + + try { + const [ + api, + { BasicTracerProvider, SimpleSpanProcessor }, + { resourceFromAttributes }, + { ATTR_SERVICE_NAME }, + ] = await Promise.all([ + import("@opentelemetry/api"), + import("@opentelemetry/sdk-trace-base"), + import("@opentelemetry/resources"), + import("@opentelemetry/semantic-conventions"), + ]); + + // Cache the API module so synchronous helpers (makeSpanContext) work + _api = api; + runtime$(OTEL_API, api); + + const resource = resourceFromAttributes({ + [ATTR_SERVICE_NAME]: telemetryConfig.serviceName, + "react_server.runtime": "edge", + }); + + let spanProcessor; + + // Try to use OTLP fetch-based exporter for edge + try { + const { OTLPTraceExporter } = + await import("@opentelemetry/exporter-trace-otlp-http"); + spanProcessor = new SimpleSpanProcessor( + new OTLPTraceExporter({ + url: `${telemetryConfig.endpoint}/v1/traces`, + }) + ); + } catch { + // If no OTLP exporter, use console + const { ConsoleSpanExporter } = + await import("@opentelemetry/sdk-trace-base"); + spanProcessor = new SimpleSpanProcessor(new ConsoleSpanExporter()); + } + + const provider = new BasicTracerProvider({ + resource, + spanProcessors: [spanProcessor], + }); + api.trace.setGlobalTracerProvider(provider); + + const tracer = api.trace.getTracer("@lazarv/react-server", "0.0.0"); + + runtime$(OTEL_TRACER, tracer); + // No meter for edge — metrics not supported in edge runtimes + runtime$(OTEL_SDK, provider); + + return provider; + } catch { + return null; + } +} + +/** + * Gracefully shut down the SDK (flush pending spans/metrics). + */ +export async function shutdownTelemetry() { + const sdk = getRuntime(OTEL_SDK); + if (sdk?.shutdown) { + await sdk.shutdown(); + } +} + +// ─── Trace-context propagation ─────────────────────────────────────────────── + +/** + * Extract W3C trace context from incoming request headers and create a root + * span. Returns { span, otelCtx } for the request. + * + * @param {string} spanName - Name for the root span + * @param {Headers|object} headers - Incoming request headers + * @param {Record} [attributes] - Initial span attributes + * @returns {Promise<{ span: any, otelCtx: any }>} + */ +export async function startRequestSpan(spanName, headers, attributes = {}) { + const tracer = getTracer(); + if (tracer === NOOP_TRACER) { + return { span: NOOP_SPAN, otelCtx: null }; + } + + const api = await otelApi(); + + // Build a carrier object from Headers + const carrier = {}; + if (headers && typeof headers.forEach === "function") { + headers.forEach((value, key) => { + carrier[key] = value; + }); + } else if (headers && typeof headers === "object") { + for (const [key, value] of Object.entries(headers)) { + carrier[key] = Array.isArray(value) ? value[0] : value; + } + } + + const extractedCtx = api.propagation.extract(api.ROOT_CONTEXT, carrier); + + const span = tracer.startSpan(spanName, { attributes }, extractedCtx); + const otelCtx = api.trace.setSpan(extractedCtx, span); + + return { span, otelCtx }; +} + +/** + * Inject trace context into outgoing response headers. + * + * @param {Headers} headers - Outgoing response headers + */ +export async function injectTraceContext(headers) { + const otelCtx = getOtelContext(); + if (!otelCtx) return; + + try { + const api = await otelApi(); + const carrier = {}; + api.propagation.inject(otelCtx, carrier); + for (const [key, value] of Object.entries(carrier)) { + headers.set(key, value); + } + } catch { + // no-op if propagation fails + } +} diff --git a/packages/react-server/telemetry/index.d.ts b/packages/react-server/telemetry/index.d.ts new file mode 100644 index 00000000..3e5189fa --- /dev/null +++ b/packages/react-server/telemetry/index.d.ts @@ -0,0 +1,156 @@ +/** + * Telemetry API for `@lazarv/react-server`. + * + * All functions are safe to call even when OpenTelemetry is not installed — + * they return no-op objects that silently discard data. + * + * @module @lazarv/react-server/telemetry + */ + +/** Minimal Span interface compatible with @opentelemetry/api Span. */ +export interface Span { + setAttribute(key: string, value: unknown): Span; + setAttributes(attributes: Record): Span; + addEvent(name: string, attributes?: Record): Span; + setStatus(status: { code: number; message?: string }): Span; + recordException(exception: unknown): void; + end(): void; + isRecording(): boolean; + spanContext(): { traceId: string; spanId: string; traceFlags: number }; + updateName(name: string): Span; +} + +/** Minimal Tracer interface compatible with @opentelemetry/api Tracer. */ +export interface Tracer { + startSpan(name: string, options?: unknown, context?: unknown): Span; + startActiveSpan unknown>( + name: string, + fn: F + ): ReturnType; +} + +/** Minimal Meter interface compatible with @opentelemetry/api Meter. */ +export interface Meter { + createCounter( + name: string, + options?: unknown + ): { add(value: number, attributes?: Record): void }; + createHistogram( + name: string, + options?: unknown + ): { record(value: number, attributes?: Record): void }; + createUpDownCounter( + name: string, + options?: unknown + ): { add(value: number, attributes?: Record): void }; +} + +/** OTel context — opaque type. */ +export type Context = unknown; + +/** + * Get the current request span from the per-request context. + * Returns a no-op span when telemetry is disabled or called outside a request. + */ +export function getSpan(): Span; + +/** + * Get the active OpenTelemetry tracer. + * Returns a no-op tracer when telemetry is disabled. + */ +export function getTracer(): Tracer; + +/** + * Get the active OpenTelemetry meter. + * Returns a no-op meter when telemetry is disabled. + */ +export function getMeter(): Meter; + +/** + * Get the OTel context for the current request. + */ +export function getOtelContext(): Context | null; + +/** + * Execute `fn` within a child span of the current request span. + * + * @param name - Span name + * @param fn - Function to execute inside the span + * @returns The return value of `fn` + * + * @example + * ```ts + * import { withSpan } from "@lazarv/react-server/telemetry"; + * + * const result = await withSpan("db.query", async (span) => { + * span.setAttribute("db.system", "postgres"); + * return await db.query("SELECT ..."); + * }); + * ``` + */ +export function withSpan( + name: string, + fn: (span: Span) => Promise | T +): Promise; + +/** + * Execute `fn` within a child span with initial attributes. + * + * @param name - Span name + * @param attributes - Initial span attributes + * @param fn - Function to execute inside the span + * @returns The return value of `fn` + * + * @example + * ```ts + * import { withSpan } from "@lazarv/react-server/telemetry"; + * + * const result = await withSpan( + * "external.api", + * { "http.url": "https://api.example.com" }, + * async (span) => { + * const res = await fetch("https://api.example.com/data"); + * span.setAttribute("http.status_code", res.status); + * return res.json(); + * } + * ); + * ``` + */ +export function withSpan( + name: string, + attributes: Record, + fn: (span: Span) => Promise | T +): Promise; + +/** + * Inject W3C trace context into outgoing response headers. + * Useful when making downstream HTTP calls that should be correlated. + */ +export function injectTraceContext(headers: Headers): Promise; + +/** + * Built-in metrics instruments. Returns `null` when telemetry is disabled. + */ +export function getMetrics(): { + httpRequestDuration: { + record(value: number, attributes?: Record): void; + }; + httpActiveRequests: { + add(value: number, attributes?: Record): void; + }; + rscRenderDuration: { + record(value: number, attributes?: Record): void; + }; + domRenderDuration: { + record(value: number, attributes?: Record): void; + }; + actionDuration: { + record(value: number, attributes?: Record): void; + }; + cacheHits: { + add(value: number, attributes?: Record): void; + }; + cacheMisses: { + add(value: number, attributes?: Record): void; + }; +} | null; diff --git a/packages/react-server/telemetry/index.mjs b/packages/react-server/telemetry/index.mjs new file mode 100644 index 00000000..908baba3 --- /dev/null +++ b/packages/react-server/telemetry/index.mjs @@ -0,0 +1,18 @@ +/** + * User-facing telemetry API for @lazarv/react-server. + * + * Import from "@lazarv/react-server/telemetry" in your server components, + * server functions, or middleware to access and extend telemetry. + * + * @module @lazarv/react-server/telemetry + */ + +export { + getSpan, + getTracer, + getMeter, + getMetrics, + withSpan, + getOtelContext, + injectTraceContext, +} from "../server/telemetry.mjs"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df096b57..be681807 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,6 +81,33 @@ importers: '@lazarv/react-server': specifier: workspace:^ version: link:../packages/react-server + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/core': + specifier: ^2.0.0 + version: 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': + specifier: ^0.200.0 + version: 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': + specifier: ^0.200.0 + version: 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: ^2.0.0 + version: 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': + specifier: ^2.0.0 + version: 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': + specifier: ^0.200.0 + version: 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': + specifier: ^2.0.0 + version: 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': + specifier: ^1.28.0 + version: 1.40.0 '@uidotdev/usehooks': specifier: ^2.4.1 version: 2.4.1 @@ -219,6 +246,33 @@ importers: '@lazarv/react-server': specifier: workspace:^ version: link:../../packages/react-server + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/core': + specifier: ^2.0.0 + version: 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': + specifier: ^0.200.0 + version: 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': + specifier: ^0.200.0 + version: 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: ^2.0.0 + version: 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': + specifier: ^2.0.0 + version: 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': + specifier: ^0.200.0 + version: 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': + specifier: ^2.0.0 + version: 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': + specifier: ^1.28.0 + version: 1.40.0 examples/mantine: dependencies: @@ -482,7 +536,7 @@ importers: version: 6.3.4 ts-jest: specifier: ^29.1.0 - version: 29.2.5(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.7.0(@types/node@20.17.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.21)(@types/node@20.17.11)(typescript@5.7.2)))(typescript@5.7.2) + version: 29.2.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest@29.7.0(@types/node@20.17.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.21)(@types/node@20.17.11)(typescript@5.7.2)))(typescript@5.7.2) ts-loader: specifier: ^9.4.3 version: 9.5.1(typescript@5.7.2)(webpack@5.97.1(@swc/core@1.11.21)) @@ -506,7 +560,7 @@ importers: version: link:../../packages/react-server react-click-away-listener: specifier: ^2.2.3 - version: 2.2.3(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 2.2.3(react-dom@0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304))(react@0.0.0-experimental-3bc2d414-20260304) devDependencies: '@types/react': specifier: ^18.3.2 @@ -607,7 +661,7 @@ importers: version: 6.3.4 react-router-dom: specifier: ^6.23.1 - version: 6.24.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 6.24.0(react-dom@0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304))(react@0.0.0-experimental-3bc2d414-20260304) sort-by: specifier: ^1.2.0 version: 1.2.0 @@ -800,11 +854,11 @@ importers: version: link:../../packages/react-server '@tanstack/react-router': specifier: ^1.40.0 - version: 1.43.3(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 1.43.3(react-dom@0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304))(react@0.0.0-experimental-3bc2d414-20260304) devDependencies: '@tanstack/router-devtools': specifier: ^1.40.0 - version: 1.43.3(@tanstack/react-router@1.43.3(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(csstype@3.1.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 1.43.3(@tanstack/react-router@1.43.3(react-dom@0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304))(react@0.0.0-experimental-3bc2d414-20260304))(csstype@3.1.3)(react-dom@0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304))(react@0.0.0-experimental-3bc2d414-20260304) '@tanstack/router-plugin': specifier: ^1.39.13 version: 1.43.1(vite@8.0.0-beta.16(@types/node@24.9.2)(jiti@2.6.1)(less@4.2.0)(sass@1.86.0)(stylus@0.62.0)(terser@5.37.0)(yaml@2.5.0)) @@ -926,7 +980,7 @@ importers: devDependencies: vitest: specifier: 4.1.0-beta.6 - version: 4.1.0-beta.6(@types/node@24.9.2)(@vitest/ui@4.1.0-beta.6)(vite@8.0.0-beta.16(@types/node@24.9.2)(jiti@2.6.1)(less@4.2.0)(sass@1.86.0)(stylus@0.62.0)(terser@5.37.0)(yaml@2.5.0)) + version: 4.1.0-beta.6(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/ui@4.1.0-beta.6)(vite@8.0.0-beta.16(@types/node@24.9.2)(jiti@2.6.1)(less@4.2.0)(sass@1.86.0)(stylus@0.62.0)(terser@5.37.0)(yaml@2.5.0)) packages/react-server: dependencies: @@ -960,6 +1014,33 @@ importers: '@modelcontextprotocol/sdk': specifier: ^1.13.0 version: 1.13.0 + '@opentelemetry/api': + specifier: ^1.0.0 + version: 1.9.0 + '@opentelemetry/core': + specifier: ^1.0.0 || ^2.0.0 + version: 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': + specifier: ^0.50.0 || ^1.0.0 + version: 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': + specifier: ^0.50.0 || ^1.0.0 + version: 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: ^1.0.0 || ^2.0.0 + version: 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': + specifier: ^1.0.0 || ^2.0.0 + version: 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': + specifier: ^0.50.0 || ^1.0.0 + version: 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': + specifier: ^1.0.0 || ^2.0.0 + version: 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': + specifier: ^1.0.0 || ^2.0.0 + version: 1.40.0 '@rollup/plugin-replace': specifier: ^5.0.2 version: 5.0.7(rollup@4.53.2) @@ -1137,7 +1218,7 @@ importers: version: 0.0.0-experimental-ab18f33d-20260220(react-dom@0.0.0-experimental-ab18f33d-20260220(react@0.0.0-experimental-ab18f33d-20260220))(react@0.0.0-experimental-ab18f33d-20260220)(webpack@5.97.1(@swc/core@1.11.21)) vitest: specifier: ^4.1.0-beta.6 - version: 4.1.0-beta.6(@types/node@24.9.2)(@vitest/ui@4.1.0-beta.6)(vite@8.0.0-beta.16(@types/node@24.9.2)(jiti@2.6.1)(less@4.2.0)(sass@1.86.0)(stylus@0.62.0)(terser@5.37.0)(yaml@2.5.0)) + version: 4.1.0-beta.6(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/ui@4.1.0-beta.6)(vite@8.0.0-beta.16(@types/node@24.9.2)(jiti@2.6.1)(less@4.2.0)(sass@1.86.0)(stylus@0.62.0)(terser@5.37.0)(yaml@2.5.0)) test: dependencies: @@ -1177,7 +1258,7 @@ importers: version: 1.53.1 vitest: specifier: ^4.1.0-beta.6 - version: 4.1.0-beta.6(@types/node@24.9.2)(@vitest/ui@4.1.0-beta.6)(vite@8.0.0-beta.16(@types/node@24.9.2)(jiti@2.6.1)(less@4.2.0)(sass@1.86.0)(stylus@0.62.0)(terser@5.37.0)(yaml@2.5.0)) + version: 4.1.0-beta.6(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/ui@4.1.0-beta.6)(vite@8.0.0-beta.16(@types/node@24.9.2)(jiti@2.6.1)(less@4.2.0)(sass@1.86.0)(stylus@0.62.0)(terser@5.37.0)(yaml@2.5.0)) packages: @@ -2453,6 +2534,15 @@ packages: '@fontsource/roboto@5.0.14': resolution: {integrity: sha512-zHAxlTTm9RuRn9/StwclFJChf3z9+fBrOxC3fw71htjHP1BgXNISwRjdJtAKAmMe5S2BzgpnjkQR93P9EZYI/Q==} + '@grpc/grpc-js@1.14.3': + resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.8.0': + resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} + engines: {node: '>=6'} + hasBin: true + '@hono/node-server@1.14.1': resolution: {integrity: sha512-vmbuM+HPinjWzPe7FFPWMMQMsbKE9gDPhaH0FFdqbGpkT5lp++tcWDTxwBl5EgS5y6JVgIaCdjeHRfQ4XRBRjQ==} engines: {node: '>=18.14.1'} @@ -2779,6 +2869,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@jsdevtools/ez-spawn@3.0.4': resolution: {integrity: sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA==} engines: {node: '>=10'} @@ -3236,6 +3329,322 @@ packages: resolution: {integrity: sha512-BVwtWE3rRXB9IugmQTfKspqjNa8q+ab73ddkV9k1Zok3XbuOxJUi4lTYk5zBZDhfWb/Y2H+RO9Iggm25gsqeow==} engines: {node: '>= 18'} + '@opentelemetry/api-logs@0.200.0': + resolution: {integrity: sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api-logs@0.50.0': + resolution: {integrity: sha512-JdZuKrhOYggqOpUljAq4WWNi5nB10PmgoF0y2CvedLGXd0kSawb/UBnWT8gg1ND3bHCNHStAIVT0ELlxJJRqrA==} + engines: {node: '>=14'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/context-async-hooks@1.23.0': + resolution: {integrity: sha512-wazGJZDRevibOJ+VgyrT+9+8sybZAxpZx2G7vy30OAtk92OpZCg7HgNxT11NUx0VBDWcRx1dOatMYGOVplQ7QA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.9.0' + + '@opentelemetry/context-async-hooks@2.0.0': + resolution: {integrity: sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.23.0': + resolution: {integrity: sha512-hdQ/a9TMzMQF/BO8Cz1juA43/L5YGtCSiKoOHmrTEf7VMDAZgy8ucpWx3eQTnQ3gBloRcWtzvcrMZABC3PTSKQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.9.0' + + '@opentelemetry/core@2.0.0': + resolution: {integrity: sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.6.0': + resolution: {integrity: sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-grpc@0.200.0': + resolution: {integrity: sha512-+3MDfa5YQPGM3WXxW9kqGD85Q7s9wlEMVNhXXG7tYFLnIeaseUt9YtCeFhEDFzfEktacdFpOtXmJuNW8cHbU5A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-http@0.200.0': + resolution: {integrity: sha512-KfWw49htbGGp9s8N4KI8EQ9XuqKJ0VG+yVYVYFiCYSjEV32qpQ5qZ9UZBzOZ6xRb+E16SXOSCT3RkqBVSABZ+g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-proto@0.200.0': + resolution: {integrity: sha512-GmahpUU/55hxfH4TP77ChOfftADsCq/nuri73I/AVLe2s4NIglvTsaACkFVZAVmnXXyPS00Fk3x27WS3yO07zA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-grpc@0.200.0': + resolution: {integrity: sha512-uHawPRvKIrhqH09GloTuYeq2BjyieYHIpiklOvxm9zhrCL2eRsnI/6g9v2BZTVtGp8tEgIa7rCQ6Ltxw6NBgew==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-http@0.200.0': + resolution: {integrity: sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-http@0.50.0': + resolution: {integrity: sha512-DMilj0pTOGxeaRPvVBil/KugvLMV5l+GzoXEWBKXYGEnfNlX+huPeMpYl+zJJBtI3Coht2KArnNOLhs2wqA3yA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-proto@0.200.0': + resolution: {integrity: sha512-E+uPj0yyvz81U9pvLZp3oHtFrEzNSqKGVkIViTQY1rH3TOobeJPSpLnTVXACnCwkPR5XeTvPnK3pZ2Kni8AFMg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-prometheus@0.200.0': + resolution: {integrity: sha512-ZYdlU9r0USuuYppiDyU2VFRA0kFl855ylnb3N/2aOlXrbA4PMCznen7gmPbetGQu7pz8Jbaf4fwvrDnVdQQXSw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.200.0': + resolution: {integrity: sha512-hmeZrUkFl1YMsgukSuHCFPYeF9df0hHoKeHUthRKFCxiURs+GwF1VuabuHmBMZnjTbsuvNjOB+JSs37Csem/5Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.50.0': + resolution: {integrity: sha512-w/NF4TrwHxx+Uz1M0rCOSVr6KgcoQPv3zF9JRqcebY2euD7ddWnLP0hE8JavyA1uq4UchnMp9faAk9n7hTCePw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/exporter-trace-otlp-http@0.200.0': + resolution: {integrity: sha512-Goi//m/7ZHeUedxTGVmEzH19NgqJY+Bzr6zXo1Rni1+hwqaksEyJ44gdlEMREu6dzX1DlAaH/qSykSVzdrdafA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-http@0.50.0': + resolution: {integrity: sha512-L7OtIMT7MsFqkmhbQlPBGRXt7152VN5esHpQEJYIBFedOEo3Da+yHpu5ojMZtPzpIvSpB5Xr5lnJUjJCbkttCA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/exporter-trace-otlp-proto@0.200.0': + resolution: {integrity: sha512-V9TDSD3PjK1OREw2iT9TUTzNYEVWJk4Nhodzhp9eiz4onDMYmPy3LaGbPv81yIR6dUb/hNp/SIhpiCHwFUq2Vg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-proto@0.50.0': + resolution: {integrity: sha512-vavD9Ow6yOLiD+ocuS/oeciCsXNdsN41aYUrEljNaLXogvnkfMhJ+JLAhOnRSpzlVtRp7Ciw2BYGdYSebR0OsA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/exporter-zipkin@1.23.0': + resolution: {integrity: sha512-2LOGvNUGONuIcWhynFaJorVyqv03uZkURScciLmOxvBf2lWTNPEj77br1dCpShIWBM+YlrH7Tc+JXAs+GC7DqA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/exporter-zipkin@2.0.0': + resolution: {integrity: sha512-icxaKZ+jZL/NHXX8Aru4HGsrdhK0MLcuRXkX5G5IRmCgoRLw+Br6I/nMVozX2xjGGwV7hw2g+4Slj8K7s4HbVg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation@0.200.0': + resolution: {integrity: sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.50.0': + resolution: {integrity: sha512-bhGhbJiZKpuu7wTaSak4hyZcFPlnDeuSF/2vglze8B4w2LubcSbbOnkVTzTs5SXtzh4Xz8eRjaNnAm+u2GYufQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.200.0': + resolution: {integrity: sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.50.0': + resolution: {integrity: sha512-JUmjmrCmE1/fc4LjCQMqLfudgSl5OpUkzx7iA94b4jgeODM7zWxUoVXL7/CT7fWf47Cn+pmKjMvTCSESqZZ3mA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/otlp-grpc-exporter-base@0.200.0': + resolution: {integrity: sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-grpc-exporter-base@0.50.0': + resolution: {integrity: sha512-J500AczSD7xEsjXpwNzSh5HQqxW73PT3CCNsi1VEWCE+8UPgVfkHYIGRHGoch35DV+CMe1svbi7gAk3e5eCSVA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/otlp-proto-exporter-base@0.50.0': + resolution: {integrity: sha512-hlbn3eZbhxoK79Sq1ddj1f7qcx+PzsPQC/SFpJvaWgTaqacCbqJmpzWDKfRRCAC7iGX2Hj/sgpf8vysazqyMOw==} + engines: {node: '>=14'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/otlp-transformer@0.200.0': + resolution: {integrity: sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.50.0': + resolution: {integrity: sha512-s0sl1Yfqd5q1Kjrf6DqXPWzErL+XHhrXOfejh4Vc/SMTNqC902xDsC8JQxbjuramWt/+hibfguIvi7Ns8VLolA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.9.0' + + '@opentelemetry/propagator-b3@1.23.0': + resolution: {integrity: sha512-cZ6rl8y2bdxYQ4e+zP2CQ+QmuPebaLBLO1skjFpj3eEu7zar+6hBzUP3llMOUupkQeQSwXz+4c8dZ26OhYfG/g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.9.0' + + '@opentelemetry/propagator-b3@2.0.0': + resolution: {integrity: sha512-blx9S2EI49Ycuw6VZq+bkpaIoiJFhsDuvFGhBIoH3vJ5oYjJ2U0s3fAM5jYft99xVIAv6HqoPtlP9gpVA2IZtA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@1.23.0': + resolution: {integrity: sha512-6iArixfgIl3ZgzeltQ5jyiKbjZygM+MbM84pXi1HL0Qs4x4Ck5rM6wEtjhZffFnlDMWEkEqrnM0xF6bTfbiMAQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.9.0' + + '@opentelemetry/propagator-jaeger@2.0.0': + resolution: {integrity: sha512-Mbm/LSFyAtQKP0AQah4AfGgsD+vsZcyreZoQ5okFBk33hU7AquU4TltgyL9dvaO8/Zkoud8/0gEvwfOZ5d7EPA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/resources@1.23.0': + resolution: {integrity: sha512-iPRLfVfcEQynYGo7e4Di+ti+YQTAY0h5mQEUJcHlU9JOqpb4x965O6PZ+wMcwYVY63G96KtdS86YCM1BF1vQZg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.9.0' + + '@opentelemetry/resources@2.0.0': + resolution: {integrity: sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/resources@2.6.0': + resolution: {integrity: sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.200.0': + resolution: {integrity: sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.50.0': + resolution: {integrity: sha512-PeUEupBB29p9nlPNqXoa1PUWNLsZnxG0DCDj3sHqzae+8y76B/A5hvZjg03ulWdnvBLYpnJslqzylG9E0IL87g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.9.0' + '@opentelemetry/api-logs': '>=0.39.1' + + '@opentelemetry/sdk-metrics@1.23.0': + resolution: {integrity: sha512-4OkvW6+wST4h6LFG23rXSTf6nmTf201h9dzq7bE0z5R9ESEVLERZz6WXwE7PSgg1gdjlaznm1jLJf8GttypFDg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.9.0' + + '@opentelemetry/sdk-metrics@2.0.0': + resolution: {integrity: sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.6.0': + resolution: {integrity: sha512-CicxWZxX6z35HR83jl+PLgtFgUrKRQ9LCXyxgenMnz5A1lgYWfAog7VtdOvGkJYyQgMNPhXQwkYrDLujk7z1Iw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-node@0.200.0': + resolution: {integrity: sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-node@0.50.0': + resolution: {integrity: sha512-LhIXHnvcnhRYcPwG9VG4G6lJ7x4ElYF6UYHHmXA7e4ZWzSUEFmAPfR1IBWv358aD1KwffcEBu7J6zeAR7lPZag==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.9.0' + + '@opentelemetry/sdk-trace-base@1.23.0': + resolution: {integrity: sha512-PzBmZM8hBomUqvCddF/5Olyyviayka44O5nDWq673np3ctnvwMOvNrsUORZjKja1zJbwEuD9niAGbnVrz3jwRQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.9.0' + + '@opentelemetry/sdk-trace-base@2.0.0': + resolution: {integrity: sha512-qQnYdX+ZCkonM7tA5iU4fSRsVxbFGml8jbxOgipRGMFHKaXKHQ30js03rTobYjKjIfnOsZSbHKWF0/0v0OQGfw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.6.0': + resolution: {integrity: sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@1.23.0': + resolution: {integrity: sha512-dwnin5Go2r6VzJZkVc9JBPupssWp7j2EFto+S7qRkwQ00WDykWeq3x2Skk7I1Jr448FeBSvGCQVPgV5e6s6O3w==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.9.0' + + '@opentelemetry/sdk-trace-node@2.0.0': + resolution: {integrity: sha512-omdilCZozUjQwY3uZRBwbaRMJ3p09l4t187Lsdf0dGMye9WKD4NGcpgZRvqhI1dwcH6og+YXQEtoO9Wx3ykilg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.23.0': + resolution: {integrity: sha512-MiqFvfOzfR31t8cc74CTP1OZfz7MbqpAnLCra8NqQoaHJX6ncIRTdYOQYBDQ2uFISDq0WY8Y9dDTWvsgzzBYRg==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.40.0': + resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} + engines: {node: '>=14'} + '@oxc-parser/binding-android-arm64@0.98.0': resolution: {integrity: sha512-/4S2BATZLxH94smwxLSvQsnzYjtyh/0mekgMnK/efCaU+92VNYir4+HOs/dvspYsWUooxPvj+AkwRUsLk9IuSg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3683,6 +4092,36 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -5305,6 +5744,9 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/shimmer@1.2.0': + resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -5567,6 +6009,12 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + acorn-import-assertions@1.9.0: + resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + deprecated: package has been renamed to acorn-import-attributes + peerDependencies: + acorn: ^8 + acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: @@ -7463,6 +7911,12 @@ packages: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} + import-in-the-middle@1.15.0: + resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + + import-in-the-middle@1.7.1: + resolution: {integrity: sha512-1LrZPDtW+atAxH42S6288qyDFNQ2YCty+2mxEPRtfazH6Z5QwkaBSTS2ods7hnVJioF6rkRfNoA6A/MstpFXLg==} + import-local@3.2.0: resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} engines: {node: '>=8'} @@ -8347,6 +8801,9 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -8725,6 +9182,9 @@ packages: module-alias@2.2.3: resolution: {integrity: sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==} + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -9447,6 +9907,10 @@ packages: prosemirror-view@1.34.2: resolution: {integrity: sha512-tPX/V2Xd70vrAGQ/V9CppJtPKnQyQMypJGlLylvdI94k6JaG+4P6fVmXPR1zc1eVTW0gq3c6zsfqwJKCRLaG9Q==} + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -9912,6 +10376,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + reserved-words@0.1.2: resolution: {integrity: sha512-0S5SrIUJ9LfpbVl4Yzij6VipUdafHrOTzvmfazSw/jeZrZtQK303OPZW+obtkaw7jQlTQppy0UvZWm9872PbRw==} @@ -10125,6 +10593,9 @@ packages: shiki@1.29.2: resolution: {integrity: sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==} + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -11858,24 +12329,24 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.29.0)': @@ -11883,19 +12354,19 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.29.0)': + '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.26.10)': @@ -11903,9 +12374,9 @@ snapshots: '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.29.0)': + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.29.0)': @@ -11913,44 +12384,44 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-typescript@7.24.7(@babel/core@7.26.10)': @@ -11958,9 +12429,9 @@ snapshots: '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-typescript@7.24.7(@babel/core@7.29.0)': + '@babel/plugin-syntax-typescript@7.24.7(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.29.0)': @@ -13198,6 +13669,18 @@ snapshots: '@fontsource/roboto@5.0.14': {} + '@grpc/grpc-js@1.14.3': + dependencies: + '@grpc/proto-loader': 0.8.0 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.8.0': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + '@hono/node-server@1.14.1(hono@4.7.8)': dependencies: hono: 4.7.8 @@ -13801,7 +14284,7 @@ snapshots: '@jest/transform@29.7.0': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.31 babel-plugin-istanbul: 6.1.1 @@ -13877,6 +14360,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@js-sdsl/ordered-map@4.4.2': {} + '@jsdevtools/ez-spawn@3.0.4': dependencies: call-me-maybe: 1.0.2 @@ -14455,6 +14940,428 @@ snapshots: '@octokit/webhooks-types': 7.6.1 aggregate-error: 3.1.0 + '@opentelemetry/api-logs@0.200.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api-logs@0.50.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/context-async-hooks@1.23.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/context-async-hooks@2.0.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@1.23.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.23.0 + + '@opentelemetry/core@2.0.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/exporter-logs-otlp-grpc@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.200.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-http@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.200.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.200.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-proto@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.200.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-grpc@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-http@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-http@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.23.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-proto@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-prometheus@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-grpc@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-grpc@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.23.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-http@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-http@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.23.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-proto@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-proto@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-proto-exporter-base': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.23.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-zipkin@1.23.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.23.0 + + '@opentelemetry/exporter-zipkin@2.0.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/instrumentation@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.200.0 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.15.0 + require-in-the-middle: 7.5.2 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.50.0 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.7.1 + require-in-the-middle: 7.5.2 + semver: 7.6.3 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/otlp-exporter-base@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.200.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-exporter-base@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-grpc-exporter-base@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.200.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-grpc-exporter-base@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.50.0(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 + + '@opentelemetry/otlp-proto-exporter-base@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.50.0(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 + + '@opentelemetry/otlp-transformer@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.200.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.0(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 + + '@opentelemetry/otlp-transformer@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.50.0 + '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.50.0(@opentelemetry/api-logs@0.50.0)(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.23.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-b3@1.23.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-b3@2.0.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-jaeger@1.23.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-jaeger@2.0.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/resources@1.23.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.23.0 + + '@opentelemetry/resources@2.0.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-logs@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.200.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-logs@0.50.0(@opentelemetry/api-logs@0.50.0)(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.50.0 + '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.23.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@1.23.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.23.0(@opentelemetry/api@1.9.0) + lodash.merge: 4.6.2 + + '@opentelemetry/sdk-metrics@2.0.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@2.6.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-node@0.200.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.200.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/sdk-node@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.50.0 + '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.50.0(@opentelemetry/api-logs@0.50.0)(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.23.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/sdk-trace-base@1.23.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.23.0 + + '@opentelemetry/sdk-trace-base@2.0.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-trace-node@1.23.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 1.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.23.0(@opentelemetry/api@1.9.0) + semver: 7.6.3 + + '@opentelemetry/sdk-trace-node@2.0.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/semantic-conventions@1.23.0': {} + + '@opentelemetry/semantic-conventions@1.40.0': {} + '@oxc-parser/binding-android-arm64@0.98.0': optional: true @@ -14713,6 +15620,29 @@ snapshots: '@popperjs/core@2.11.8': {} + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -15671,21 +16601,21 @@ snapshots: '@tanstack/query-core': 5.54.1 react: 19.2.1 - '@tanstack/react-router@1.43.3(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + '@tanstack/react-router@1.43.3(react-dom@0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304))(react@0.0.0-experimental-3bc2d414-20260304)': dependencies: '@tanstack/history': 1.41.0 - '@tanstack/react-store': 0.2.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1) - react: 19.2.1 - react-dom: 19.2.1(react@19.2.1) + '@tanstack/react-store': 0.2.1(react-dom@0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304))(react@0.0.0-experimental-3bc2d414-20260304) + react: 0.0.0-experimental-3bc2d414-20260304 + react-dom: 0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304) tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/react-store@0.2.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + '@tanstack/react-store@0.2.1(react-dom@0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304))(react@0.0.0-experimental-3bc2d414-20260304)': dependencies: '@tanstack/store': 0.1.3 - react: 19.2.1 - react-dom: 19.2.1(react@19.2.1) - use-sync-external-store: 1.2.2(react@19.2.1) + react: 0.0.0-experimental-3bc2d414-20260304 + react-dom: 0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304) + use-sync-external-store: 1.2.2(react@0.0.0-experimental-3bc2d414-20260304) '@tanstack/react-table@8.21.3(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: @@ -15693,14 +16623,14 @@ snapshots: react: 19.2.1 react-dom: 19.2.1(react@19.2.1) - '@tanstack/router-devtools@1.43.3(@tanstack/react-router@1.43.3(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(csstype@3.1.3)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + '@tanstack/router-devtools@1.43.3(@tanstack/react-router@1.43.3(react-dom@0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304))(react@0.0.0-experimental-3bc2d414-20260304))(csstype@3.1.3)(react-dom@0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304))(react@0.0.0-experimental-3bc2d414-20260304)': dependencies: - '@tanstack/react-router': 1.43.3(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@tanstack/react-router': 1.43.3(react-dom@0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304))(react@0.0.0-experimental-3bc2d414-20260304) clsx: 2.1.1 date-fns: 2.30.0 goober: 2.1.14(csstype@3.1.3) - react: 19.2.1 - react-dom: 19.2.1(react@19.2.1) + react: 0.0.0-experimental-3bc2d414-20260304 + react-dom: 0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304) transitivePeerDependencies: - csstype @@ -15884,7 +16814,7 @@ snapshots: '@tiptap/pm': 2.6.6 '@types/use-sync-external-store': 0.0.6 react-dom: 19.2.1(react@19.2.1) - use-sync-external-store: 1.2.2(react@19.2.1) + use-sync-external-store: 1.2.2(react@0.0.0-experimental-3bc2d414-20260304) '@tiptap/starter-kit@2.6.6': dependencies: @@ -16168,6 +17098,8 @@ snapshots: '@types/node': 20.17.11 '@types/send': 0.17.4 + '@types/shimmer@1.2.0': {} + '@types/stack-utils@2.0.3': {} '@types/stats.js@0.17.4': {} @@ -16375,7 +17307,7 @@ snapshots: magicast: 0.5.2 obug: 2.1.1 tinyrainbow: 3.0.3 - vitest: 4.1.0-beta.6(@types/node@24.9.2)(@vitest/ui@4.1.0-beta.6)(vite@8.0.0-beta.16(@types/node@24.9.2)(jiti@2.6.1)(less@4.2.0)(sass@1.86.0)(stylus@0.62.0)(terser@5.37.0)(yaml@2.5.0)) + vitest: 4.1.0-beta.6(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/ui@4.1.0-beta.6)(vite@8.0.0-beta.16(@types/node@24.9.2)(jiti@2.6.1)(less@4.2.0)(sass@1.86.0)(stylus@0.62.0)(terser@5.37.0)(yaml@2.5.0)) transitivePeerDependencies: - supports-color @@ -16391,7 +17323,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.1.0-beta.6(@types/node@24.9.2)(@vitest/ui@4.1.0-beta.6)(vite@8.0.0-beta.16(@types/node@24.9.2)(jiti@2.6.1)(less@4.2.0)(sass@1.86.0)(stylus@0.62.0)(terser@5.37.0)(yaml@2.5.0)) + vitest: 4.1.0-beta.6(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/ui@4.1.0-beta.6)(vite@8.0.0-beta.16(@types/node@24.9.2)(jiti@2.6.1)(less@4.2.0)(sass@1.86.0)(stylus@0.62.0)(terser@5.37.0)(yaml@2.5.0)) '@vitest/expect@4.1.0-beta.6': dependencies: @@ -16437,7 +17369,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vitest: 4.1.0-beta.6(@types/node@24.9.2)(@vitest/ui@4.1.0-beta.6)(vite@8.0.0-beta.16(@types/node@24.9.2)(jiti@2.6.1)(less@4.2.0)(sass@1.86.0)(stylus@0.62.0)(terser@5.37.0)(yaml@2.5.0)) + vitest: 4.1.0-beta.6(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/ui@4.1.0-beta.6)(vite@8.0.0-beta.16(@types/node@24.9.2)(jiti@2.6.1)(less@4.2.0)(sass@1.86.0)(stylus@0.62.0)(terser@5.37.0)(yaml@2.5.0)) '@vitest/utils@4.1.0-beta.6': dependencies: @@ -16558,6 +17490,10 @@ snapshots: mime-types: 3.0.1 negotiator: 1.0.0 + acorn-import-assertions@1.9.0(acorn@8.14.1): + dependencies: + acorn: 8.14.1 + acorn-import-attributes@1.9.5(acorn@8.14.1): dependencies: acorn: 8.14.1 @@ -16793,13 +17729,13 @@ snapshots: transitivePeerDependencies: - supports-color - babel-jest@29.7.0(@babel/core@7.29.0): + babel-jest@29.7.0(@babel/core@7.28.5): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@jest/transform': 29.7.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.29.0) + babel-preset-jest: 29.6.3(@babel/core@7.28.5) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -16818,8 +17754,8 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/template': 7.27.0 + '@babel/types': 7.28.5 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.6 @@ -16829,30 +17765,30 @@ snapshots: cosmiconfig: 7.1.0 resolve: 1.22.8 - babel-preset-current-node-syntax@1.1.0(@babel/core@7.29.0): + babel-preset-current-node-syntax@1.1.0(@babel/core@7.28.5): dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) - '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.29.0) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) - - babel-preset-jest@29.6.3(@babel/core@7.29.0): + '@babel/core': 7.28.5 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.5) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.5) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.5) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.28.5) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.5) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.5) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.5) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.5) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.5) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.5) + + babel-preset-jest@29.6.3(@babel/core@7.28.5): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.29.0) + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.28.5) bail@2.0.2: {} @@ -18708,6 +19644,20 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-in-the-middle@1.15.0: + dependencies: + acorn: 8.14.1 + acorn-import-attributes: 1.9.5(acorn@8.14.1) + cjs-module-lexer: 1.4.1 + module-details-from-path: 1.0.4 + + import-in-the-middle@1.7.1: + dependencies: + acorn: 8.14.1 + acorn-import-assertions: 1.9.0(acorn@8.14.1) + cjs-module-lexer: 1.4.1 + module-details-from-path: 1.0.4 + import-local@3.2.0: dependencies: pkg-dir: 4.2.0 @@ -19075,10 +20025,10 @@ snapshots: jest-config@29.7.0(@types/node@20.17.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.21)(@types/node@20.17.11)(typescript@5.7.2)): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.0) + babel-jest: 29.7.0(@babel/core@7.28.5) chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 @@ -19260,15 +20210,15 @@ snapshots: jest-snapshot@29.7.0: dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@babel/generator': 7.27.0 - '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.29.0) - '@babel/plugin-syntax-typescript': 7.24.7(@babel/core@7.29.0) + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.28.5) + '@babel/plugin-syntax-typescript': 7.24.7(@babel/core@7.28.5) '@babel/types': 7.27.0 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.29.0) + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.28.5) chalk: 4.1.2 expect: 29.7.0 graceful-fs: 4.2.11 @@ -19696,6 +20646,8 @@ snapshots: strip-ansi: 7.1.0 wrap-ansi: 9.0.0 + long@5.3.2: {} + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -20330,6 +21282,8 @@ snapshots: module-alias@2.2.3: {} + module-details-from-path@1.0.4: {} + mrmime@2.0.0: {} ms@2.0.0: {} @@ -21125,6 +22079,21 @@ snapshots: prosemirror-state: 1.4.3 prosemirror-transform: 1.10.0 + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.17.11 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -21204,10 +22173,10 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - react-click-away-listener@2.2.3(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + react-click-away-listener@2.2.3(react-dom@0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304))(react@0.0.0-experimental-3bc2d414-20260304): dependencies: - react: 19.2.1 - react-dom: 19.2.1(react@19.2.1) + react: 0.0.0-experimental-3bc2d414-20260304 + react-dom: 0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304) react-clientside-effect@1.2.6(react@19.2.1): dependencies: @@ -21352,17 +22321,17 @@ snapshots: optionalDependencies: '@types/react': 19.2.2 - react-router-dom@6.24.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + react-router-dom@6.24.0(react-dom@0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304))(react@0.0.0-experimental-3bc2d414-20260304): dependencies: '@remix-run/router': 1.17.0 - react: 19.2.1 - react-dom: 19.2.1(react@19.2.1) - react-router: 6.24.0(react@19.2.1) + react: 0.0.0-experimental-3bc2d414-20260304 + react-dom: 0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304) + react-router: 6.24.0(react@0.0.0-experimental-3bc2d414-20260304) - react-router@6.24.0(react@19.2.1): + react-router@6.24.0(react@0.0.0-experimental-3bc2d414-20260304): dependencies: '@remix-run/router': 1.17.0 - react: 19.2.1 + react: 0.0.0-experimental-3bc2d414-20260304 react-server-dom-webpack@0.0.0-experimental-3bc2d414-20260304(react-dom@0.0.0-experimental-3bc2d414-20260304(react@0.0.0-experimental-3bc2d414-20260304))(react@0.0.0-experimental-3bc2d414-20260304)(webpack@5.97.1(@swc/core@1.11.21)): dependencies: @@ -21732,6 +22701,14 @@ snapshots: require-from-string@2.0.2: {} + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + reserved-words@0.1.2: {} resolve-cwd@3.0.0: @@ -22037,6 +23014,8 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + shimmer@1.2.1: {} + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -22586,7 +23565,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.5(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.7.0(@types/node@20.17.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.21)(@types/node@20.17.11)(typescript@5.7.2)))(typescript@5.7.2): + ts-jest@29.2.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest@29.7.0(@types/node@20.17.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.11.21)(@types/node@20.17.11)(typescript@5.7.2)))(typescript@5.7.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 @@ -22600,10 +23579,10 @@ snapshots: typescript: 5.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.5 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.0) + babel-jest: 29.7.0(@babel/core@7.28.5) ts-loader@9.5.1(typescript@5.7.2)(webpack@5.97.1(@swc/core@1.11.21)): dependencies: @@ -22949,9 +23928,9 @@ snapshots: optionalDependencies: '@types/react': 19.2.2 - use-sync-external-store@1.2.2(react@19.2.1): + use-sync-external-store@1.2.2(react@0.0.0-experimental-3bc2d414-20260304): dependencies: - react: 19.2.1 + react: 0.0.0-experimental-3bc2d414-20260304 use-sync-external-store@1.6.0(react@19.2.1): dependencies: @@ -23092,7 +24071,7 @@ snapshots: terser: 5.37.0 yaml: 2.5.0 - vitest@4.1.0-beta.6(@types/node@24.9.2)(@vitest/ui@4.1.0-beta.6)(vite@8.0.0-beta.16(@types/node@24.9.2)(jiti@2.6.1)(less@4.2.0)(sass@1.86.0)(stylus@0.62.0)(terser@5.37.0)(yaml@2.5.0)): + vitest@4.1.0-beta.6(@opentelemetry/api@1.9.0)(@types/node@24.9.2)(@vitest/ui@4.1.0-beta.6)(vite@8.0.0-beta.16(@types/node@24.9.2)(jiti@2.6.1)(less@4.2.0)(sass@1.86.0)(stylus@0.62.0)(terser@5.37.0)(yaml@2.5.0)): dependencies: '@vitest/expect': 4.1.0-beta.6 '@vitest/mocker': 4.1.0-beta.6(vite@8.0.0-beta.16(@types/node@24.9.2)(jiti@2.6.1)(less@4.2.0)(sass@1.86.0)(stylus@0.62.0)(terser@5.37.0)(yaml@2.5.0)) @@ -23115,6 +24094,7 @@ snapshots: vite: 8.0.0-beta.16(@types/node@24.9.2)(jiti@2.6.1)(less@4.2.0)(sass@1.86.0)(stylus@0.62.0)(terser@5.37.0)(yaml@2.5.0) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.0 '@types/node': 24.9.2 '@vitest/ui': 4.1.0-beta.6(vitest@4.1.0-beta.6) transitivePeerDependencies: diff --git a/test/README.md b/test/README.md new file mode 100644 index 00000000..0688576e --- /dev/null +++ b/test/README.md @@ -0,0 +1,201 @@ +# Testing + +This directory contains the test suite for `@lazarv/react-server`. Tests use [Vitest](https://vitest.dev/) as the test runner and [Playwright](https://playwright.dev/) (Chromium) for browser-based integration tests. + +## Quick start + +From the **repository root**: + +```bash +# Run the full test suite (dev + build modes) +pnpm test +``` + +This delegates to the `test` workspace package via `pnpm --filter ./test test`. + +## Available scripts + +All scripts below are defined in `test/package.json` and should be run from the `test/` directory (or via `pnpm --filter ./test