@open-mercato/telemetry 0.8.1-develop.7275.1.f772b944d4 → 0.8.1-develop.7295.1.d0e0e33014

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +22 -2
  3. package/README.md +76 -1
  4. package/build.mjs +1 -1
  5. package/dist/browser/BrowserTelemetry.js +110 -0
  6. package/dist/browser/BrowserTelemetry.js.map +7 -0
  7. package/dist/browser/config.js +5 -0
  8. package/dist/browser/config.js.map +7 -0
  9. package/dist/browser/index.js +10 -0
  10. package/dist/browser/index.js.map +7 -0
  11. package/dist/browser/propagator.js +19 -0
  12. package/dist/browser/propagator.js.map +7 -0
  13. package/dist/browser/server.js +95 -0
  14. package/dist/browser/server.js.map +7 -0
  15. package/dist/modules/telemetry/__integration__/TC-TELEMETRY-001.spec.js +34 -0
  16. package/dist/modules/telemetry/__integration__/TC-TELEMETRY-001.spec.js.map +7 -0
  17. package/dist/modules/telemetry/__integration__/TC-TELEMETRY-002.spec.js +118 -0
  18. package/dist/modules/telemetry/__integration__/TC-TELEMETRY-002.spec.js.map +7 -0
  19. package/dist/modules/telemetry/api/browser-traces/route.js +141 -0
  20. package/dist/modules/telemetry/api/browser-traces/route.js.map +7 -0
  21. package/dist/modules/telemetry/index.js +11 -0
  22. package/dist/modules/telemetry/index.js.map +7 -0
  23. package/dist/provider/otlp-provider.js +3 -2
  24. package/dist/provider/otlp-provider.js.map +2 -2
  25. package/dist/trace-headers.js +7 -0
  26. package/dist/trace-headers.js.map +7 -0
  27. package/package.json +29 -3
  28. package/src/__tests__/browser-server-config.test.ts +265 -0
  29. package/src/__tests__/browser-traces-route.test.ts +234 -0
  30. package/src/__tests__/otlp-integration.test.ts +54 -1
  31. package/src/browser/BrowserTelemetry.tsx +194 -0
  32. package/src/browser/__tests__/browser-telemetry.test.tsx +325 -0
  33. package/src/browser/config.ts +21 -0
  34. package/src/browser/index.ts +15 -0
  35. package/src/browser/propagator.ts +40 -0
  36. package/src/browser/server.ts +170 -0
  37. package/src/modules/telemetry/__integration__/TC-TELEMETRY-001.spec.ts +50 -0
  38. package/src/modules/telemetry/__integration__/TC-TELEMETRY-002.spec.ts +179 -0
  39. package/src/modules/telemetry/api/browser-traces/route.ts +206 -0
  40. package/src/modules/telemetry/index.ts +15 -0
  41. package/src/provider/otlp-provider.ts +3 -2
  42. package/src/trace-headers.ts +16 -0
@@ -0,0 +1,118 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { login } from "@open-mercato/core/helpers/integration/auth";
3
+ const TRACES_PATH = "/api/telemetry/browser-traces";
4
+ const OPT_IN_COOKIE = "om_test_browser_telemetry";
5
+ function recordIngest(page) {
6
+ const records = [];
7
+ page.on("response", async (response) => {
8
+ if (!response.url().includes(TRACES_PATH)) return;
9
+ const request = response.request();
10
+ if (request.method() !== "POST") return;
11
+ records.push({ status: response.status(), body: request.postData() ?? "" });
12
+ });
13
+ return records;
14
+ }
15
+ function recordApiGets(page) {
16
+ const urls = [];
17
+ page.on("response", (response) => {
18
+ const request = response.request();
19
+ if (request.method() !== "GET") return;
20
+ if (response.status() !== 200) return;
21
+ try {
22
+ const { pathname } = new URL(response.url());
23
+ if (!pathname.startsWith("/api/") || pathname.startsWith(TRACES_PATH)) return;
24
+ urls.push(response.url());
25
+ } catch {
26
+ }
27
+ });
28
+ return urls;
29
+ }
30
+ async function otelApiIsRegistered(page) {
31
+ return page.evaluate(
32
+ () => Object.getOwnPropertySymbols(globalThis).some((symbol) => symbol.toString().includes("opentelemetry.js.api"))
33
+ );
34
+ }
35
+ test.describe("TC-TELEMETRY-002: browser RUM runtime", () => {
36
+ test("exports document-load and fetch spans without breaking either fetch path", async ({ page, baseURL }) => {
37
+ const origin = new URL(baseURL ?? "http://localhost:3000").origin;
38
+ await page.context().addCookies([{ name: OPT_IN_COOKIE, value: "on", url: origin }]);
39
+ const ingest = recordIngest(page);
40
+ const apiGets = recordApiGets(page);
41
+ await login(page, "admin");
42
+ await page.goto("/backend");
43
+ await expect.poll(() => otelApiIsRegistered(page), {
44
+ message: "the web SDK must boot once the server hands the layout a non-null config",
45
+ timeout: 15e3
46
+ }).toBe(true);
47
+ const patchState = await page.evaluate(() => {
48
+ const w = window;
49
+ return {
50
+ hasStash: typeof w.__omOriginalFetch === "function",
51
+ // Identical would mean the wrapper was handed back to itself — an infinite recursion on
52
+ // the first request through `apiFetch`.
53
+ stashIsWrapper: w.__omOriginalFetch === window.fetch
54
+ };
55
+ });
56
+ expect(patchState.hasStash, "@open-mercato/ui installs the fetch wrapper on every backoffice page").toBe(true);
57
+ expect(patchState.stashIsWrapper, "instrumenting the wrapper instead of the native fetch recurses forever").toBe(false);
58
+ await expect.poll(() => apiGets.length, {
59
+ message: "the backoffice must still reach its own API through the patched fetch",
60
+ timeout: 3e4
61
+ }).toBeGreaterThan(0);
62
+ await page.goto("/backend/customers/people");
63
+ await expect(page.locator('table, [role="grid"]').first()).toBeVisible({ timeout: 3e4 });
64
+ const replayUrl = apiGets[apiGets.length - 1];
65
+ const replay = await page.evaluate(async (url) => {
66
+ const w = window;
67
+ const withTimeout = async (call) => {
68
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 1e4));
69
+ return (await Promise.race([call, timeout])).status;
70
+ };
71
+ return {
72
+ viaWindow: await withTimeout(window.fetch(url)),
73
+ viaStash: await withTimeout(w.__omOriginalFetch(url))
74
+ };
75
+ }, replayUrl);
76
+ expect(replay.viaWindow, `window.fetch must still reach ${replayUrl} through the framework wrapper`).toBe(200);
77
+ expect(replay.viaStash, `the instrumented native fetch must still reach ${replayUrl}`).toBe(200);
78
+ await page.evaluate(() => {
79
+ Object.defineProperty(document, "visibilityState", { value: "hidden", configurable: true });
80
+ document.dispatchEvent(new Event("visibilitychange"));
81
+ });
82
+ await expect.poll(() => ingest.length, {
83
+ message: "the exporter must post its batches to the same-origin proxy",
84
+ timeout: 2e4
85
+ }).toBeGreaterThan(0);
86
+ for (const record of ingest) {
87
+ expect([202, 204], `unexpected ingest status ${record.status}`).toContain(record.status);
88
+ }
89
+ const spanNames = ingest.flatMap((record) => {
90
+ try {
91
+ const payload = JSON.parse(record.body);
92
+ for (const resourceSpan of payload.resourceSpans ?? []) {
93
+ const serviceName = resourceSpan.resource?.attributes?.find((a) => a.key === "service.name");
94
+ expect(
95
+ serviceName?.value?.stringValue,
96
+ "browser spans must carry their own service name so they sort apart from server spans"
97
+ ).toMatch(/-browser$/);
98
+ }
99
+ return (payload.resourceSpans ?? []).flatMap((r) => (r.scopeSpans ?? []).flatMap((s) => s.spans ?? [])).map((s) => s.name);
100
+ } catch {
101
+ return [];
102
+ }
103
+ });
104
+ expect(spanNames, "document-load instrumentation must produce the navigation span").toContain("documentLoad");
105
+ expect(spanNames, "document-load instrumentation must produce the document fetch span").toContain("documentFetch");
106
+ });
107
+ test("ships nothing at all without the opt-in \u2014 the default path must cost a disabled app zero bytes", async ({ page }) => {
108
+ const ingest = recordIngest(page);
109
+ await login(page, "admin");
110
+ await page.goto("/backend");
111
+ await page.goto("/backend/customers/people");
112
+ await expect(page.locator('table, [role="grid"]').first()).toBeVisible({ timeout: 3e4 });
113
+ await page.waitForTimeout(5e3);
114
+ expect(await otelApiIsRegistered(page), "the web SDK chunk must never be requested while RUM is off").toBe(false);
115
+ expect(ingest, "a disabled app must not post a single batch").toHaveLength(0);
116
+ });
117
+ });
118
+ //# sourceMappingURL=TC-TELEMETRY-002.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/telemetry/__integration__/TC-TELEMETRY-002.spec.ts"],
4
+ "sourcesContent": ["import { expect, test, type Page, type Request as PlaywrightRequest } from '@playwright/test'\nimport { login } from '@open-mercato/core/helpers/integration/auth'\n\n/**\n * TC-TELEMETRY-002: browser RUM in a real browser.\n *\n * TC-TELEMETRY-001 drives the proxy route with `request.post` \u2014 a useful API contract test that\n * never opens a page. This spec covers what only a browser can: that the SDK actually boots, that\n * the `__omOriginalFetch` patch dance leaves both fetch paths working (a recursion here hangs every\n * API call in the backoffice), that batches reach the proxy, and \u2014 the mirror image \u2014 that a\n * default environment ships none of it.\n *\n * Browser RUM is normally an environment-wide env switch, which a spec cannot flip for one page.\n * The integration environment therefore sets `OM_TEST_BROWSER_TELEMETRY_MODE=opt-in` on top of\n * `OM_TEST_MODE=1`, and only then does the server honour an `om_test_browser_telemetry=on` cookie.\n * Both variables are absent in production. Same escape hatch the auth rate limiter already uses.\n *\n * No collector is configured here, so the proxy answers 204 (accept-and-drop) rather than 202 \u2014\n * which is why the span assertions read the batch the *browser posted* instead of anything a sink\n * received. That is the stronger assertion anyway: it is the browser's output under test.\n */\n\nconst TRACES_PATH = '/api/telemetry/browser-traces'\nconst OPT_IN_COOKIE = 'om_test_browser_telemetry'\n\ntype IngestRecord = { status: number; body: string }\n\n/** Records every proxy call the page makes, with the payload the exporter actually produced. */\nfunction recordIngest(page: Page): IngestRecord[] {\n const records: IngestRecord[] = []\n page.on('response', async (response) => {\n if (!response.url().includes(TRACES_PATH)) return\n const request: PlaywrightRequest = response.request()\n if (request.method() !== 'POST') return\n records.push({ status: response.status(), body: request.postData() ?? '' })\n })\n return records\n}\n\n/**\n * Records the same-origin API GETs the backoffice itself made and that answered 200. Replaying a\n * real one beats naming an endpoint here: the set of routes is not this spec's business, and a\n * path that quietly stops existing would fail the test for the wrong reason.\n */\nfunction recordApiGets(page: Page): string[] {\n const urls: string[] = []\n page.on('response', (response) => {\n const request = response.request()\n if (request.method() !== 'GET') return\n if (response.status() !== 200) return\n try {\n const { pathname } = new URL(response.url())\n if (!pathname.startsWith('/api/') || pathname.startsWith(TRACES_PATH)) return\n urls.push(response.url())\n } catch {\n // A non-absolute or exotic URL is simply not a candidate for replay.\n }\n })\n return urls\n}\n\n/** The web SDK registers itself on this global symbol; its presence is the \"SDK booted\" signal. */\nasync function otelApiIsRegistered(page: Page): Promise<boolean> {\n return page.evaluate(() =>\n Object.getOwnPropertySymbols(globalThis).some((symbol) => symbol.toString().includes('opentelemetry.js.api')),\n )\n}\n\ntest.describe('TC-TELEMETRY-002: browser RUM runtime', () => {\n test('exports document-load and fetch spans without breaking either fetch path', async ({ page, baseURL }) => {\n const origin = new URL(baseURL ?? 'http://localhost:3000').origin\n await page.context().addCookies([{ name: OPT_IN_COOKIE, value: 'on', url: origin }])\n const ingest = recordIngest(page)\n const apiGets = recordApiGets(page)\n\n await login(page, 'admin')\n await page.goto('/backend')\n await expect.poll(() => otelApiIsRegistered(page), {\n message: 'the web SDK must boot once the server hands the layout a non-null config',\n timeout: 15_000,\n }).toBe(true)\n\n // The patch dance, asserted where it actually runs. `@open-mercato/ui`'s api utils stash the\n // pristine fetch on `__omOriginalFetch` and put a wrapper on `window.fetch`; the SDK has to\n // instrument the stash rather than the wrapper, or direct `apiFetch` callers (DataTable,\n // CrudForm, react-query) produce no spans at all.\n const patchState = await page.evaluate(() => {\n const w = window as Window & { __omOriginalFetch?: typeof window.fetch }\n return {\n hasStash: typeof w.__omOriginalFetch === 'function',\n // Identical would mean the wrapper was handed back to itself \u2014 an infinite recursion on\n // the first request through `apiFetch`.\n stashIsWrapper: w.__omOriginalFetch === window.fetch,\n }\n })\n expect(patchState.hasStash, '@open-mercato/ui installs the fetch wrapper on every backoffice page').toBe(true)\n expect(patchState.stashIsWrapper, 'instrumenting the wrapper instead of the native fetch recurses forever').toBe(false)\n\n // A DataTable page drives `apiFetch` (which reads the instrumented stash) end to end. Waiting\n // on the API call it makes, rather than on a DOM role, keeps this assertion about the thing\n // under test: that the swap left `apiFetch` able to reach the API at all.\n await expect.poll(() => apiGets.length, {\n message: 'the backoffice must still reach its own API through the patched fetch',\n timeout: 30_000,\n }).toBeGreaterThan(0)\n await page.goto('/backend/customers/people')\n await expect(page.locator('table, [role=\"grid\"]').first()).toBeVisible({ timeout: 30_000 })\n\n // Replay one of the page's OWN successful GETs down both paths. A hang here IS the recursion\n // bug, hence the tight budget; a non-200 means the swap broke the wrapper's auth handling.\n const replayUrl = apiGets[apiGets.length - 1]\n const replay = await page.evaluate(async (url: string) => {\n const w = window as Window & { __omOriginalFetch?: typeof window.fetch }\n const withTimeout = async (call: Promise<Response>) => {\n const timeout = new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), 10_000))\n return (await Promise.race([call, timeout])).status\n }\n return {\n viaWindow: await withTimeout(window.fetch(url)),\n viaStash: await withTimeout(w.__omOriginalFetch!(url)),\n }\n }, replayUrl)\n expect(replay.viaWindow, `window.fetch must still reach ${replayUrl} through the framework wrapper`).toBe(200)\n expect(replay.viaStash, `the instrumented native fetch must still reach ${replayUrl}`).toBe(200)\n\n // Exports are batched on a 3s delay, and a flush is forced when the page is hidden.\n await page.evaluate(() => {\n Object.defineProperty(document, 'visibilityState', { value: 'hidden', configurable: true })\n document.dispatchEvent(new Event('visibilitychange'))\n })\n await expect.poll(() => ingest.length, {\n message: 'the exporter must post its batches to the same-origin proxy',\n timeout: 20_000,\n }).toBeGreaterThan(0)\n\n // Every answer must be one the OTLP exporter does NOT retry (429/502/503/504 would storm).\n for (const record of ingest) {\n expect([202, 204], `unexpected ingest status ${record.status}`).toContain(record.status)\n }\n\n const spanNames = ingest.flatMap((record) => {\n try {\n const payload = JSON.parse(record.body) as {\n resourceSpans?: Array<{\n resource?: { attributes?: Array<{ key: string; value?: { stringValue?: string } }> }\n scopeSpans?: Array<{ spans?: Array<{ name: string }> }>\n }>\n }\n for (const resourceSpan of payload.resourceSpans ?? []) {\n const serviceName = resourceSpan.resource?.attributes?.find((a) => a.key === 'service.name')\n expect(\n serviceName?.value?.stringValue,\n 'browser spans must carry their own service name so they sort apart from server spans',\n ).toMatch(/-browser$/)\n }\n return (payload.resourceSpans ?? []).flatMap((r) => (r.scopeSpans ?? []).flatMap((s) => s.spans ?? [])).map((s) => s.name)\n } catch {\n return []\n }\n })\n expect(spanNames, 'document-load instrumentation must produce the navigation span').toContain('documentLoad')\n expect(spanNames, 'document-load instrumentation must produce the document fetch span').toContain('documentFetch')\n })\n\n test('ships nothing at all without the opt-in \u2014 the default path must cost a disabled app zero bytes', async ({ page }) => {\n // No cookie: this is the shipped default, and the mirror image of the case above. It is the\n // assertion that protects the \"zero client-bundle cost while off\" promise from regressing.\n const ingest = recordIngest(page)\n\n await login(page, 'admin')\n await page.goto('/backend')\n await page.goto('/backend/customers/people')\n await expect(page.locator('table, [role=\"grid\"]').first()).toBeVisible({ timeout: 30_000 })\n await page.waitForTimeout(5_000)\n\n expect(await otelApiIsRegistered(page), 'the web SDK chunk must never be requested while RUM is off').toBe(false)\n expect(ingest, 'a disabled app must not post a single batch').toHaveLength(0)\n })\n})\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,141 @@
1
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
2
+ import { createLogger } from "@open-mercato/shared/lib/logger";
3
+ import {
4
+ resolveBrowserTelemetryConfig,
5
+ resolveCollectorHeaders,
6
+ resolveCollectorTracesUrl
7
+ } from "../../../../browser/server.js";
8
+ const logger = createLogger("telemetry").child({ component: "browser-traces" });
9
+ const metadata = {
10
+ // Sending a span is not a privileged action — every authenticated backoffice user may do it, so
11
+ // no feature grant. `requireAuth: true` is what keeps an unauthenticated span-ingest endpoint off
12
+ // a public host.
13
+ //
14
+ // Deliberately NO dispatcher-level `rateLimit`. That limiter keys on the client IP and falls back
15
+ // to a single `'global'` bucket whenever `RATE_LIMIT_TRUST_PROXY_DEPTH` is 0 (the default), so one
16
+ // flat budget would be shared by every browser in the deployment — and it answers `429`, which is
17
+ // in the OTLP exporter's retryable set (`429 || 502 || 503 || 504`). That combination would make
18
+ // a handful of busy tabs trigger the retry storm the handler below exists to prevent, under
19
+ // normal load rather than during an outage. The budget is enforced per authenticated user inside
20
+ // the handler instead, where it can answer with a non-retryable status.
21
+ POST: {
22
+ requireAuth: true
23
+ }
24
+ };
25
+ const INGEST_BUDGET = {
26
+ points: 400,
27
+ duration: 60,
28
+ keyPrefix: "telemetry_browser_traces"
29
+ };
30
+ const openApi = {
31
+ tags: ["Telemetry"],
32
+ summary: "Same-origin OTLP/HTTP trace ingest for browser RUM spans \u2014 forwards to the collector"
33
+ };
34
+ const MAX_BODY_BYTES = 1e6;
35
+ const COLLECTOR_TIMEOUT_MS = 5e3;
36
+ async function POST(req, ctx) {
37
+ const declaredLength = Number(req.headers.get("content-length"));
38
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_BODY_BYTES) {
39
+ return new Response(null, { status: 413 });
40
+ }
41
+ if (!resolveBrowserTelemetryConfig({ cookieHeader: req.headers.get("cookie") })) {
42
+ return new Response(null, { status: 204 });
43
+ }
44
+ if (await isOverIngestBudget(ctx?.auth?.sub)) return new Response(null, { status: 204 });
45
+ const target = resolveCollectorTracesUrl();
46
+ if (!target) return new Response(null, { status: 204 });
47
+ const body = await readCappedBody(req);
48
+ if (body === null) return new Response(null, { status: 413 });
49
+ if (body.byteLength === 0) return new Response(null, { status: 202 });
50
+ try {
51
+ const res = await fetch(target, {
52
+ method: "POST",
53
+ headers: {
54
+ // Credential and operator headers first, so the forwarded content headers below always
55
+ // win: an operator whose OTEL_EXPORTER_OTLP_HEADERS happens to carry a `content-type` must
56
+ // not silently relabel the body the browser actually sent.
57
+ ...resolveCollectorHeaders(),
58
+ // The browser exporter posts JSON; keep whatever it declared so a future switch to the
59
+ // protobuf exporter needs no change here.
60
+ "content-type": req.headers.get("content-type") ?? "application/json",
61
+ // Forward compression verbatim — dropping it would hand the collector a gzip blob
62
+ // labelled as plain JSON the moment the exporter enables compression.
63
+ ...req.headers.get("content-encoding") ? { "content-encoding": req.headers.get("content-encoding") } : {}
64
+ },
65
+ body,
66
+ signal: AbortSignal.timeout(COLLECTOR_TIMEOUT_MS),
67
+ cache: "no-store"
68
+ });
69
+ if (!res.ok) {
70
+ logger.warn("telemetry.browser.export.rejected", {
71
+ status: res.status,
72
+ bytes: body.byteLength
73
+ });
74
+ return new Response(null, { status: 202 });
75
+ }
76
+ } catch (err) {
77
+ logger.warn("telemetry.browser.export.failed", {
78
+ error: err instanceof Error ? err.message : String(err),
79
+ bytes: body.byteLength
80
+ });
81
+ return new Response(null, { status: 202 });
82
+ }
83
+ return new Response(null, { status: 202 });
84
+ }
85
+ async function isOverIngestBudget(userId) {
86
+ if (!userId) return false;
87
+ try {
88
+ const container = await createRequestContainer();
89
+ const limiter = tryResolve(container, "rateLimiterService");
90
+ if (!limiter) return false;
91
+ const result = await limiter.consume(userId, INGEST_BUDGET);
92
+ if (result.allowed || result.degraded) return false;
93
+ logger.warn("telemetry.browser.export.throttled", {
94
+ userId,
95
+ budgetPerMinute: INGEST_BUDGET.points
96
+ });
97
+ return true;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+ function tryResolve(container, name) {
103
+ try {
104
+ return container.resolve(name) ?? null;
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+ async function readCappedBody(req) {
110
+ if (!req.body) return new ArrayBuffer(0);
111
+ const reader = req.body.getReader();
112
+ const chunks = [];
113
+ let total = 0;
114
+ try {
115
+ for (; ; ) {
116
+ const { done, value } = await reader.read();
117
+ if (done) break;
118
+ total += value.byteLength;
119
+ if (total > MAX_BODY_BYTES) {
120
+ await reader.cancel();
121
+ return null;
122
+ }
123
+ chunks.push(value);
124
+ }
125
+ } catch {
126
+ return null;
127
+ }
128
+ const out = new Uint8Array(total);
129
+ let offset = 0;
130
+ for (const chunk of chunks) {
131
+ out.set(chunk, offset);
132
+ offset += chunk.byteLength;
133
+ }
134
+ return out.buffer;
135
+ }
136
+ export {
137
+ POST,
138
+ metadata,
139
+ openApi
140
+ };
141
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/telemetry/api/browser-traces/route.ts"],
4
+ "sourcesContent": ["import type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'\nimport type { RateLimitConfig } from '@open-mercato/shared/lib/ratelimit/types'\nimport {\n resolveBrowserTelemetryConfig,\n resolveCollectorHeaders,\n resolveCollectorTracesUrl,\n} from '../../../../browser/server'\n\nconst logger = createLogger('telemetry').child({ component: 'browser-traces' })\n\nexport const metadata = {\n // Sending a span is not a privileged action \u2014 every authenticated backoffice user may do it, so\n // no feature grant. `requireAuth: true` is what keeps an unauthenticated span-ingest endpoint off\n // a public host.\n //\n // Deliberately NO dispatcher-level `rateLimit`. That limiter keys on the client IP and falls back\n // to a single `'global'` bucket whenever `RATE_LIMIT_TRUST_PROXY_DEPTH` is 0 (the default), so one\n // flat budget would be shared by every browser in the deployment \u2014 and it answers `429`, which is\n // in the OTLP exporter's retryable set (`429 || 502 || 503 || 504`). That combination would make\n // a handful of busy tabs trigger the retry storm the handler below exists to prevent, under\n // normal load rather than during an outage. The budget is enforced per authenticated user inside\n // the handler instead, where it can answer with a non-retryable status.\n POST: {\n requireAuth: true,\n },\n}\n\n/**\n * Per-user ingest budget, sized from the export cadence rather than picked round: the\n * `BatchSpanProcessor` in `BrowserTelemetry.tsx` uses `scheduledDelayMillis: 3_000`, i.e. at most\n * 20 export requests per minute per open tab. 400/min therefore covers ~20 concurrent backoffice\n * tabs for one user before anything is shed, and a user who exceeds it can only ever starve\n * themselves \u2014 never the rest of the deployment, which the shared IP bucket could.\n */\nconst INGEST_BUDGET: RateLimitConfig = {\n points: 400,\n duration: 60,\n keyPrefix: 'telemetry_browser_traces',\n}\n\nexport const openApi = {\n tags: ['Telemetry'],\n summary: 'Same-origin OTLP/HTTP trace ingest for browser RUM spans \u2014 forwards to the collector',\n}\n\n/** Batched browser exports are a few KB; 1 MB is a generous ceiling that still refuses abuse. */\nconst MAX_BODY_BYTES = 1_000_000\n/** The collector is expected to be one hop away (in-cluster or a tunnel). */\nconst COLLECTOR_TIMEOUT_MS = 5_000\n\n/**\n * Proxies a browser OTLP/HTTP trace payload to the environment's collector, adding the\n * credential server-side.\n *\n * Deliberately fire-and-forget: a telemetry pipeline must never be able to degrade the app it\n * measures, so every failure path returns a success-ish status and is logged instead. Dropped spans\n * are cheaper than a retry storm from every open browser tab.\n *\n * That is why a collector failure answers 202 rather than 502: 502 is in the OTLP exporter's\n * retryable set (`429 || 502 || 503 || 504`), so a collector outage would make every open tab\n * retry each batch with backoff \u2014 the exact amplification this contract rules out \u2014 and surface a\n * failed request in the user's devtools on every attempt. The outage is observable in the server\n * log instead, where it belongs.\n *\n * Raw `fetch` is correct here (the AGENTS.md rule targets app-to-own-API calls that have a framework\n * primitive) \u2014 this is a server-to-collector hop over plain OTLP/HTTP with no DI-provided client.\n */\nexport async function POST(req: Request, ctx?: { auth?: AuthContext | null }): Promise<Response> {\n // Refuse on the declared size BEFORE anything else: App Router route handlers have no default\n // body limit, so reading first would let an authenticated client force a large allocation. A\n // caller can lie or omit content-length, hence the second check inside the read \u2014 but the honest\n // path (the OTel exporter always sets it) never allocates. Checked even while disabled so the\n // size cap is an unconditional contract of the endpoint.\n const declaredLength = Number(req.headers.get('content-length'))\n if (Number.isFinite(declaredLength) && declaredLength > MAX_BODY_BYTES) {\n return new Response(null, { status: 413 })\n }\n\n // Not configured / disabled for this environment: accept and drop, so a stale client that still\n // has the SDK loaded doesn't retry or surface errors.\n if (!resolveBrowserTelemetryConfig({ cookieHeader: req.headers.get('cookie') })) {\n return new Response(null, { status: 204 })\n }\n\n // Over the per-user budget: drop the batch with the same non-retryable accept-and-drop status a\n // disabled environment uses. A `429` here would be read as retryable by the exporter and turn a\n // single looping tab into a backoff storm.\n if (await isOverIngestBudget(ctx?.auth?.sub)) return new Response(null, { status: 204 })\n\n const target = resolveCollectorTracesUrl()\n if (!target) return new Response(null, { status: 204 })\n\n const body = await readCappedBody(req)\n if (body === null) return new Response(null, { status: 413 })\n if (body.byteLength === 0) return new Response(null, { status: 202 })\n\n try {\n const res = await fetch(target, {\n method: 'POST',\n headers: {\n // Credential and operator headers first, so the forwarded content headers below always\n // win: an operator whose OTEL_EXPORTER_OTLP_HEADERS happens to carry a `content-type` must\n // not silently relabel the body the browser actually sent.\n ...resolveCollectorHeaders(),\n // The browser exporter posts JSON; keep whatever it declared so a future switch to the\n // protobuf exporter needs no change here.\n 'content-type': req.headers.get('content-type') ?? 'application/json',\n // Forward compression verbatim \u2014 dropping it would hand the collector a gzip blob\n // labelled as plain JSON the moment the exporter enables compression.\n ...(req.headers.get('content-encoding')\n ? { 'content-encoding': req.headers.get('content-encoding') as string }\n : {}),\n },\n body,\n signal: AbortSignal.timeout(COLLECTOR_TIMEOUT_MS),\n cache: 'no-store',\n })\n if (!res.ok) {\n logger.warn('telemetry.browser.export.rejected', {\n status: res.status,\n bytes: body.byteLength,\n })\n return new Response(null, { status: 202 })\n }\n } catch (err) {\n logger.warn('telemetry.browser.export.failed', {\n error: err instanceof Error ? err.message : String(err),\n bytes: body.byteLength,\n })\n return new Response(null, { status: 202 })\n }\n\n return new Response(null, { status: 202 })\n}\n\n/**\n * Consumes one point of the authenticated user's ingest budget and reports whether the batch\n * should be shed.\n *\n * Fails open in every degraded case \u2014 no user id, no limiter registered, a limiter whose backing\n * store is unreachable, or a throw on the way there. An unenforced telemetry budget is strictly\n * better than a telemetry endpoint that starts refusing traffic because its limiter is sick.\n */\nasync function isOverIngestBudget(userId: string | null | undefined): Promise<boolean> {\n if (!userId) return false\n try {\n const container = await createRequestContainer()\n const limiter = tryResolve<RateLimiterService>(container, 'rateLimiterService')\n if (!limiter) return false\n const result = await limiter.consume(userId, INGEST_BUDGET)\n if (result.allowed || result.degraded) return false\n logger.warn('telemetry.browser.export.throttled', {\n userId,\n budgetPerMinute: INGEST_BUDGET.points,\n })\n return true\n } catch {\n return false\n }\n}\n\nfunction tryResolve<T>(container: { resolve: (name: string) => unknown }, name: string): T | null {\n try {\n return (container.resolve(name) as T) ?? null\n } catch {\n return null\n }\n}\n\n/**\n * Reads the body but abandons it the moment it exceeds the cap, so a client that omits or\n * understates `content-length` still cannot make us buffer an arbitrary payload. Returns `null`\n * when the cap is breached.\n */\nasync function readCappedBody(req: Request): Promise<ArrayBuffer | null> {\n if (!req.body) return new ArrayBuffer(0)\n const reader = req.body.getReader()\n const chunks: Uint8Array[] = []\n let total = 0\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n total += value.byteLength\n if (total > MAX_BODY_BYTES) {\n await reader.cancel()\n return null\n }\n chunks.push(value)\n }\n } catch {\n return null\n }\n const out = new Uint8Array(total)\n let offset = 0\n for (const chunk of chunks) {\n out.set(chunk, offset)\n offset += chunk.byteLength\n }\n // `.buffer` is exactly the allocation above, so the cast is sound and keeps `fetch` happy\n // (a generic Uint8Array is not assignable to BodyInit under this TS lib).\n return out.buffer as ArrayBuffer\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,11 @@
1
+ const metadata = {
2
+ name: "telemetry",
3
+ title: "Telemetry",
4
+ version: "0.1.0",
5
+ description: "Same-origin OTLP proxy for browser RUM spans (document load, fetch, user interaction), forwarding to the collector configured for the environment.",
6
+ author: "Open Mercato Team"
7
+ };
8
+ export {
9
+ metadata
10
+ };
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/telemetry/index.ts"],
4
+ "sourcesContent": ["import type { ModuleInfo } from '@open-mercato/shared/modules/registry'\n\n// Browser RUM support. The browser usually cannot reach the OTLP collector\n// directly (in-cluster service, auth in front of it), so this module owns the\n// same-origin proxy the browser exporter posts to. Server-side telemetry stays\n// with the `@open-mercato/telemetry` facade \u2014 this module never initializes it.\nexport const metadata: ModuleInfo = {\n name: 'telemetry',\n title: 'Telemetry',\n version: '0.1.0',\n description:\n 'Same-origin OTLP proxy for browser RUM spans (document load, fetch, user interaction), ' +\n 'forwarding to the collector configured for the environment.',\n author: 'Open Mercato Team',\n}\n"],
5
+ "mappings": "AAMO,MAAM,WAAuB;AAAA,EAClC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,aACE;AAAA,EAEF,QAAQ;AACV;",
6
+ "names": []
7
+ }
@@ -27,6 +27,7 @@ import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
27
27
  import { PgInstrumentation } from "@opentelemetry/instrumentation-pg";
28
28
  import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
29
29
  import { readTelemetryEnv } from "../env.js";
30
+ import { BACKUP_TRACEPARENT_HEADER, BACKUP_TRACESTATE_HEADER } from "../trace-headers.js";
30
31
  import { redactAttributes, redactPii } from "../facade/redact.js";
31
32
  import { serializeError } from "../facade/serialize.js";
32
33
  import { runSpan } from "./run-span.js";
@@ -34,8 +35,8 @@ const TRACER_NAME = "open-mercato";
34
35
  const queuePropagator = new W3CTraceContextPropagator();
35
36
  const W3C_TRACEPARENT = "traceparent";
36
37
  const W3C_TRACESTATE = "tracestate";
37
- const BACKUP_TRACEPARENT = "x-original-traceparent";
38
- const BACKUP_TRACESTATE = "x-original-tracestate";
38
+ const BACKUP_TRACEPARENT = BACKUP_TRACEPARENT_HEADER;
39
+ const BACKUP_TRACESTATE = BACKUP_TRACESTATE_HEADER;
39
40
  const backupHeaderPropagator = {
40
41
  inject(ctx, carrier, setter) {
41
42
  const tmp = {};
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/provider/otlp-provider.ts"],
4
- "sourcesContent": ["/**\n * The ONLY file in this package that imports `@opentelemetry/*`. Loaded via\n * dynamic `import()` from `init.ts` (so the heavy SDK never loads when telemetry\n * is off), and the OTEL packages live in `optionalDependencies`.\n *\n * Serves any OTLP backend \u2014 the endpoint + headers come from the standard\n * `OTEL_EXPORTER_OTLP_*` env vars, read by the exporters directly.\n */\nimport {\n trace,\n metrics,\n context,\n isSpanContextValid,\n defaultTextMapGetter,\n defaultTextMapSetter,\n ROOT_CONTEXT,\n SpanStatusCode,\n SpanKind as OtelSpanKind,\n type Link,\n type Span as OtelApiSpan,\n type SpanOptions as OtelSpanOptions,\n type TextMapPropagator,\n type Counter,\n type Histogram,\n type Gauge,\n} from '@opentelemetry/api'\nimport { W3CTraceContextPropagator } from '@opentelemetry/core'\nimport { logs, SeverityNumber } from '@opentelemetry/api-logs'\nimport { NodeSDK } from '@opentelemetry/sdk-node'\nimport type { Instrumentation } from '@opentelemetry/instrumentation'\nimport { resourceFromAttributes } from '@opentelemetry/resources'\nimport { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'\nimport {\n BatchSpanProcessor,\n ParentBasedSampler,\n TraceIdRatioBasedSampler,\n type SpanProcessor,\n} from '@opentelemetry/sdk-trace-node'\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'\nimport { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'\nimport { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'\nimport { BatchLogRecordProcessor, type LogRecordProcessor } from '@opentelemetry/sdk-logs'\nimport { PeriodicExportingMetricReader, type IMetricReader } from '@opentelemetry/sdk-metrics'\nimport { PgInstrumentation } from '@opentelemetry/instrumentation-pg'\nimport { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici'\n\nimport type {\n Attributes,\n AttributeValue,\n LogLevel,\n LogRecord,\n MetricPoint,\n Span,\n SpanKind,\n SpanOptions,\n TelemetryProvider,\n TelemetrySignal,\n TraceCarrier,\n TraceContext,\n} from '../types'\nimport { readTelemetryEnv } from '../env'\nimport { redactAttributes, redactPii } from '../facade/redact'\nimport { serializeError } from '../facade/serialize'\nimport { runSpan } from './run-span'\n\nconst TRACER_NAME = 'open-mercato'\n\n/**\n * Queue/event trace propagation rides a dedicated W3C propagator on the payload\n * `_trace` carrier \u2014 deliberately NOT the global propagator below.\n */\nconst queuePropagator = new W3CTraceContextPropagator()\n\nconst W3C_TRACEPARENT = 'traceparent'\nconst W3C_TRACESTATE = 'tracestate'\n// Backup headers that mirror the W3C context but a proxy/LB won't rewrite.\nconst BACKUP_TRACEPARENT = 'x-original-traceparent'\nconst BACKUP_TRACESTATE = 'x-original-tracestate'\n\n/**\n * Global propagator: standard W3C plus a backup copy\n * (`x-original-traceparent`), with explicit inbound trust.\n *\n * The problem: a load balancer (e.g. GCP's) reads the inbound `traceparent`,\n * creates its own span, and **rewrites** `traceparent` to point at that span \u2014\n * which our backend never sees. With plain W3C extraction every request becomes a\n * child of that unexported span: no request is a root, and root-span / trace-group\n * views come up empty.\n *\n * The fix (the industry \"backup header\" pattern): on inject we also write\n * `x-original-traceparent`, which the LB leaves untouched. A backup header is\n * still caller-controlled at an HTTP boundary, so its mere\n * presence is not proof that one of our services created it. Extraction ignores\n * both standard and backup headers by default.\n *\n * `TELEMETRY_TRUST_INBOUND_TRACE=true` enables both extraction paths for a\n * deployment behind a trusted upstream. Our dedicated queue/event carrier is\n * unaffected \u2014 it uses `queuePropagator` directly, not this global one.\n */\nconst backupHeaderPropagator: TextMapPropagator = {\n inject(ctx, carrier, setter) {\n // Inject into a plain temp carrier so we can mirror the values regardless of\n // the real carrier's setter shape, then copy both standard + backup headers.\n const tmp: Record<string, string> = {}\n queuePropagator.inject(ctx, tmp, defaultTextMapSetter)\n for (const [key, value] of Object.entries(tmp)) setter.set(carrier, key, value)\n if (tmp[W3C_TRACEPARENT]) setter.set(carrier, BACKUP_TRACEPARENT, tmp[W3C_TRACEPARENT])\n if (tmp[W3C_TRACESTATE]) setter.set(carrier, BACKUP_TRACESTATE, tmp[W3C_TRACESTATE])\n },\n extract(ctx, carrier, getter) {\n // Both the standard and backup headers are caller-controlled at an HTTP\n // boundary. Trust neither unless the deployment explicitly opts in.\n if (!readTelemetryEnv().trustInboundTrace) return ctx\n const first = (value: string | string[] | undefined): string | undefined =>\n Array.isArray(value) ? value[0] : value\n const backupTraceparent = first(getter.get(carrier, BACKUP_TRACEPARENT))\n if (backupTraceparent) {\n const backupCarrier: Record<string, string> = { [W3C_TRACEPARENT]: backupTraceparent }\n const backupTracestate = first(getter.get(carrier, BACKUP_TRACESTATE))\n if (backupTracestate) backupCarrier[W3C_TRACESTATE] = backupTracestate\n return queuePropagator.extract(ctx, backupCarrier, defaultTextMapGetter)\n }\n return queuePropagator.extract(ctx, carrier, getter)\n },\n fields: () => [...queuePropagator.fields(), BACKUP_TRACEPARENT, BACKUP_TRACESTATE],\n}\n\nfunction cleanAttributes(attributes?: Attributes): Record<string, AttributeValue> {\n const out: Record<string, AttributeValue> = {}\n if (!attributes) return out\n for (const [key, value] of Object.entries(redactAttributes(attributes))) {\n if (value !== undefined) out[key] = value\n }\n return out\n}\n\nconst SPAN_KIND: Record<SpanKind, OtelSpanKind> = {\n internal: OtelSpanKind.INTERNAL,\n server: OtelSpanKind.SERVER,\n client: OtelSpanKind.CLIENT,\n producer: OtelSpanKind.PRODUCER,\n consumer: OtelSpanKind.CONSUMER,\n}\n\n/**\n * Resolve `links` carriers to span contexts. Extraction runs against\n * `ROOT_CONTEXT`, never the active one \u2014 otherwise an empty carrier would\n * silently link a span to its own ambient parent instead of yielding no link.\n */\nfunction spanLinks(carriers?: TraceCarrier[]): Link[] | undefined {\n if (!carriers?.length) return undefined\n const links: Link[] = []\n for (const carrier of carriers) {\n const linked = trace.getSpanContext(queuePropagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter))\n if (linked && isSpanContextValid(linked)) links.push({ context: linked })\n }\n return links.length ? links : undefined\n}\n\n/**\n * Translate the vendor-neutral options to OTEL's. `root: true` makes OTEL drop\n * the parent from the context it samples against, so the span starts a new trace\n * and the `ParentBasedSampler` falls through to its ratio-based root sampler.\n */\nfunction toOtelSpanOptions(options: SpanOptions): OtelSpanOptions {\n return {\n kind: options.kind ? SPAN_KIND[options.kind] : undefined,\n attributes: cleanAttributes(options.attributes),\n root: options.root,\n links: spanLinks(options.links),\n }\n}\n\nconst SEVERITY: Record<LogLevel, SeverityNumber> = {\n debug: SeverityNumber.DEBUG,\n info: SeverityNumber.INFO,\n warn: SeverityNumber.WARN,\n error: SeverityNumber.ERROR,\n}\n\n/** Adapts an OTEL span to the vendor-neutral `Span` interface. */\nclass OtelSpan implements Span {\n constructor(private readonly span: OtelApiSpan) {}\n setAttribute(key: string, value: AttributeValue): void {\n const redacted = cleanAttributes({ [key]: value })[key]\n if (redacted !== undefined) this.span.setAttribute(key, redacted)\n }\n setAttributes(attributes: Attributes): void {\n this.span.setAttributes(cleanAttributes(attributes))\n }\n updateName(name: string): void {\n this.span.updateName(name)\n }\n recordException(error: unknown): void {\n // Redact message + stack before they leave the process (Privacy): the auto\n // record-on-throw path (run-span) and reportError both pass raw errors here.\n this.span.recordException(serializeError(error))\n }\n setStatus(status: 'ok' | 'error', message?: string): void {\n this.span.setStatus({\n code: status === 'error' ? SpanStatusCode.ERROR : SpanStatusCode.OK,\n message: message ? redactPii(message) : undefined,\n })\n }\n end(): void {\n this.span.end()\n }\n}\n\n/**\n * Test seam: override the SDK's exporters/instrumentations so tests can use\n * in-memory exporters (deterministic, no network) instead of the OTLP defaults.\n * Production passes nothing \u2014 the OTLP defaults apply.\n */\nexport type OtlpProviderOptions = {\n spanProcessors?: SpanProcessor[]\n logRecordProcessors?: LogRecordProcessor[]\n metricReaders?: IMetricReader[]\n instrumentations?: Instrumentation[]\n}\n\n/**\n * PII guard (telemetry spec Privacy / R12). The single source of truth for pg\n * instrumentation config: `enhancedDatabaseReporting: false` means bound SQL\n * parameter VALUES are NOT captured \u2014 only the statement shape ($1, $2). User\n * data lives in those params, so this must never be flipped on. Asserted by a\n * regression test.\n */\nexport const PG_INSTRUMENTATION_OPTIONS = { enhancedDatabaseReporting: false } as const\n\nexport class OtlpProvider implements TelemetryProvider {\n /** The configured backend name (signoz | newrelic | otlp) \u2014 all OTLP, vendor differs only by endpoint. */\n readonly name: string\n readonly supports: readonly TelemetrySignal[] = ['traces', 'metrics', 'logs', 'errors']\n\n private sdk: NodeSDK | undefined\n private readonly instruments = new Map<string, Counter | Histogram | Gauge>()\n\n constructor(private readonly options: OtlpProviderOptions = {}, name = 'otlp') {\n this.name = name\n }\n\n async start(): Promise<void> {\n if (this.sdk) return\n const env = readTelemetryEnv()\n\n this.sdk = new NodeSDK({\n resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: env.serviceName }),\n // Global propagation roots requests by default. Both standard and backup\n // inbound headers are honored only when TELEMETRY_TRUST_INBOUND_TRACE is\n // explicitly enabled behind trusted infrastructure.\n textMapPropagator: backupHeaderPropagator,\n // Parent-based so child spans follow the trace's sampling decision; root\n // spans sample at the configured ratio.\n sampler: new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(env.samplingRatio) }),\n spanProcessors: this.options.spanProcessors ?? [new BatchSpanProcessor(new OTLPTraceExporter())],\n logRecordProcessors: this.options.logRecordProcessors ?? [\n new BatchLogRecordProcessor({ exporter: new OTLPLogExporter() }),\n ],\n metricReaders: this.options.metricReaders ?? [\n new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter() }),\n ],\n instrumentations: this.options.instrumentations ?? [\n new PgInstrumentation(PG_INSTRUMENTATION_OPTIONS),\n new UndiciInstrumentation(),\n ],\n })\n this.sdk.start()\n }\n\n async shutdown(): Promise<void> {\n if (!this.sdk) return\n await this.sdk.shutdown()\n this.sdk = undefined\n }\n\n runInSpan<T>(name: string, options: SpanOptions, fn: (span: Span) => T): T {\n const tracer = trace.getTracer(TRACER_NAME)\n return tracer.startActiveSpan(name, toOtelSpanOptions(options), (otelSpan) =>\n runSpan(new OtelSpan(otelSpan), fn),\n )\n }\n\n activeSpan(): Span | undefined {\n const active = trace.getActiveSpan()\n return active ? new OtelSpan(active) : undefined\n }\n\n activeTraceContext(): TraceContext | undefined {\n const ctx = trace.getActiveSpan()?.spanContext()\n if (!ctx || !isSpanContextValid(ctx)) return undefined\n return { traceId: ctx.traceId, spanId: ctx.spanId }\n }\n\n inject(carrier: TraceCarrier): void {\n queuePropagator.inject(context.active(), carrier, defaultTextMapSetter)\n }\n\n runInRemoteSpan<T>(carrier: TraceCarrier, name: string, options: SpanOptions, fn: (span: Span) => T): T {\n const parent = queuePropagator.extract(context.active(), carrier, defaultTextMapGetter)\n const tracer = trace.getTracer(TRACER_NAME)\n return context.with(parent, () =>\n tracer.startActiveSpan(name, toOtelSpanOptions(options), (otelSpan) =>\n runSpan(new OtelSpan(otelSpan), fn),\n ),\n )\n }\n\n emitLog(record: LogRecord): void {\n const attributes: Record<string, AttributeValue> = cleanAttributes(record.attributes)\n if (record.error) {\n attributes['exception.type'] = redactPii(record.error.name)\n attributes['exception.message'] = redactPii(record.error.message)\n if (record.error.stack) attributes['exception.stacktrace'] = redactPii(record.error.stack)\n }\n logs.getLogger(TRACER_NAME).emit({\n severityNumber: SEVERITY[record.level],\n severityText: record.level,\n body: redactPii(record.message),\n attributes,\n })\n }\n\n recordMetric(point: MetricPoint): void {\n const labels = cleanAttributes(point.labels)\n const instrument = this.getInstrument(point)\n if (point.kind === 'counter') (instrument as Counter).add(point.value, labels)\n else (instrument as Histogram | Gauge).record(point.value, labels)\n }\n\n private getInstrument(point: MetricPoint): Counter | Histogram | Gauge {\n const key = `${point.kind}:${point.name}`\n let instrument = this.instruments.get(key)\n if (!instrument) {\n const meter = metrics.getMeter(TRACER_NAME)\n const opts = point.unit ? { unit: point.unit } : undefined\n instrument =\n point.kind === 'counter'\n ? meter.createCounter(point.name, opts)\n : point.kind === 'histogram'\n ? meter.createHistogram(point.name, opts)\n : meter.createGauge(point.name, opts)\n this.instruments.set(key, instrument)\n }\n return instrument\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * The ONLY file in this package that imports `@opentelemetry/*`. Loaded via\n * dynamic `import()` from `init.ts` (so the heavy SDK never loads when telemetry\n * is off), and the OTEL packages live in `optionalDependencies`.\n *\n * Serves any OTLP backend \u2014 the endpoint + headers come from the standard\n * `OTEL_EXPORTER_OTLP_*` env vars, read by the exporters directly.\n */\nimport {\n trace,\n metrics,\n context,\n isSpanContextValid,\n defaultTextMapGetter,\n defaultTextMapSetter,\n ROOT_CONTEXT,\n SpanStatusCode,\n SpanKind as OtelSpanKind,\n type Link,\n type Span as OtelApiSpan,\n type SpanOptions as OtelSpanOptions,\n type TextMapPropagator,\n type Counter,\n type Histogram,\n type Gauge,\n} from '@opentelemetry/api'\nimport { W3CTraceContextPropagator } from '@opentelemetry/core'\nimport { logs, SeverityNumber } from '@opentelemetry/api-logs'\nimport { NodeSDK } from '@opentelemetry/sdk-node'\nimport type { Instrumentation } from '@opentelemetry/instrumentation'\nimport { resourceFromAttributes } from '@opentelemetry/resources'\nimport { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'\nimport {\n BatchSpanProcessor,\n ParentBasedSampler,\n TraceIdRatioBasedSampler,\n type SpanProcessor,\n} from '@opentelemetry/sdk-trace-node'\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'\nimport { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'\nimport { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'\nimport { BatchLogRecordProcessor, type LogRecordProcessor } from '@opentelemetry/sdk-logs'\nimport { PeriodicExportingMetricReader, type IMetricReader } from '@opentelemetry/sdk-metrics'\nimport { PgInstrumentation } from '@opentelemetry/instrumentation-pg'\nimport { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici'\n\nimport type {\n Attributes,\n AttributeValue,\n LogLevel,\n LogRecord,\n MetricPoint,\n Span,\n SpanKind,\n SpanOptions,\n TelemetryProvider,\n TelemetrySignal,\n TraceCarrier,\n TraceContext,\n} from '../types'\nimport { readTelemetryEnv } from '../env'\nimport { BACKUP_TRACEPARENT_HEADER, BACKUP_TRACESTATE_HEADER } from '../trace-headers'\nimport { redactAttributes, redactPii } from '../facade/redact'\nimport { serializeError } from '../facade/serialize'\nimport { runSpan } from './run-span'\n\nconst TRACER_NAME = 'open-mercato'\n\n/**\n * Queue/event trace propagation rides a dedicated W3C propagator on the payload\n * `_trace` carrier \u2014 deliberately NOT the global propagator below.\n */\nconst queuePropagator = new W3CTraceContextPropagator()\n\nconst W3C_TRACEPARENT = 'traceparent'\nconst W3C_TRACESTATE = 'tracestate'\n// Backup headers that mirror the W3C context but a proxy/LB won't rewrite.\nconst BACKUP_TRACEPARENT = BACKUP_TRACEPARENT_HEADER\nconst BACKUP_TRACESTATE = BACKUP_TRACESTATE_HEADER\n\n/**\n * Global propagator: standard W3C plus a backup copy\n * (`x-original-traceparent`), with explicit inbound trust.\n *\n * The problem: a load balancer (e.g. GCP's) reads the inbound `traceparent`,\n * creates its own span, and **rewrites** `traceparent` to point at that span \u2014\n * which our backend never sees. With plain W3C extraction every request becomes a\n * child of that unexported span: no request is a root, and root-span / trace-group\n * views come up empty.\n *\n * The fix (the industry \"backup header\" pattern): on inject we also write\n * `x-original-traceparent`, which the LB leaves untouched. A backup header is\n * still caller-controlled at an HTTP boundary, so its mere\n * presence is not proof that one of our services created it. Extraction ignores\n * both standard and backup headers by default.\n *\n * `TELEMETRY_TRUST_INBOUND_TRACE=true` enables both extraction paths for a\n * deployment behind a trusted upstream. Our dedicated queue/event carrier is\n * unaffected \u2014 it uses `queuePropagator` directly, not this global one.\n */\nconst backupHeaderPropagator: TextMapPropagator = {\n inject(ctx, carrier, setter) {\n // Inject into a plain temp carrier so we can mirror the values regardless of\n // the real carrier's setter shape, then copy both standard + backup headers.\n const tmp: Record<string, string> = {}\n queuePropagator.inject(ctx, tmp, defaultTextMapSetter)\n for (const [key, value] of Object.entries(tmp)) setter.set(carrier, key, value)\n if (tmp[W3C_TRACEPARENT]) setter.set(carrier, BACKUP_TRACEPARENT, tmp[W3C_TRACEPARENT])\n if (tmp[W3C_TRACESTATE]) setter.set(carrier, BACKUP_TRACESTATE, tmp[W3C_TRACESTATE])\n },\n extract(ctx, carrier, getter) {\n // Both the standard and backup headers are caller-controlled at an HTTP\n // boundary. Trust neither unless the deployment explicitly opts in.\n if (!readTelemetryEnv().trustInboundTrace) return ctx\n const first = (value: string | string[] | undefined): string | undefined =>\n Array.isArray(value) ? value[0] : value\n const backupTraceparent = first(getter.get(carrier, BACKUP_TRACEPARENT))\n if (backupTraceparent) {\n const backupCarrier: Record<string, string> = { [W3C_TRACEPARENT]: backupTraceparent }\n const backupTracestate = first(getter.get(carrier, BACKUP_TRACESTATE))\n if (backupTracestate) backupCarrier[W3C_TRACESTATE] = backupTracestate\n return queuePropagator.extract(ctx, backupCarrier, defaultTextMapGetter)\n }\n return queuePropagator.extract(ctx, carrier, getter)\n },\n fields: () => [...queuePropagator.fields(), BACKUP_TRACEPARENT, BACKUP_TRACESTATE],\n}\n\nfunction cleanAttributes(attributes?: Attributes): Record<string, AttributeValue> {\n const out: Record<string, AttributeValue> = {}\n if (!attributes) return out\n for (const [key, value] of Object.entries(redactAttributes(attributes))) {\n if (value !== undefined) out[key] = value\n }\n return out\n}\n\nconst SPAN_KIND: Record<SpanKind, OtelSpanKind> = {\n internal: OtelSpanKind.INTERNAL,\n server: OtelSpanKind.SERVER,\n client: OtelSpanKind.CLIENT,\n producer: OtelSpanKind.PRODUCER,\n consumer: OtelSpanKind.CONSUMER,\n}\n\n/**\n * Resolve `links` carriers to span contexts. Extraction runs against\n * `ROOT_CONTEXT`, never the active one \u2014 otherwise an empty carrier would\n * silently link a span to its own ambient parent instead of yielding no link.\n */\nfunction spanLinks(carriers?: TraceCarrier[]): Link[] | undefined {\n if (!carriers?.length) return undefined\n const links: Link[] = []\n for (const carrier of carriers) {\n const linked = trace.getSpanContext(queuePropagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter))\n if (linked && isSpanContextValid(linked)) links.push({ context: linked })\n }\n return links.length ? links : undefined\n}\n\n/**\n * Translate the vendor-neutral options to OTEL's. `root: true` makes OTEL drop\n * the parent from the context it samples against, so the span starts a new trace\n * and the `ParentBasedSampler` falls through to its ratio-based root sampler.\n */\nfunction toOtelSpanOptions(options: SpanOptions): OtelSpanOptions {\n return {\n kind: options.kind ? SPAN_KIND[options.kind] : undefined,\n attributes: cleanAttributes(options.attributes),\n root: options.root,\n links: spanLinks(options.links),\n }\n}\n\nconst SEVERITY: Record<LogLevel, SeverityNumber> = {\n debug: SeverityNumber.DEBUG,\n info: SeverityNumber.INFO,\n warn: SeverityNumber.WARN,\n error: SeverityNumber.ERROR,\n}\n\n/** Adapts an OTEL span to the vendor-neutral `Span` interface. */\nclass OtelSpan implements Span {\n constructor(private readonly span: OtelApiSpan) {}\n setAttribute(key: string, value: AttributeValue): void {\n const redacted = cleanAttributes({ [key]: value })[key]\n if (redacted !== undefined) this.span.setAttribute(key, redacted)\n }\n setAttributes(attributes: Attributes): void {\n this.span.setAttributes(cleanAttributes(attributes))\n }\n updateName(name: string): void {\n this.span.updateName(name)\n }\n recordException(error: unknown): void {\n // Redact message + stack before they leave the process (Privacy): the auto\n // record-on-throw path (run-span) and reportError both pass raw errors here.\n this.span.recordException(serializeError(error))\n }\n setStatus(status: 'ok' | 'error', message?: string): void {\n this.span.setStatus({\n code: status === 'error' ? SpanStatusCode.ERROR : SpanStatusCode.OK,\n message: message ? redactPii(message) : undefined,\n })\n }\n end(): void {\n this.span.end()\n }\n}\n\n/**\n * Test seam: override the SDK's exporters/instrumentations so tests can use\n * in-memory exporters (deterministic, no network) instead of the OTLP defaults.\n * Production passes nothing \u2014 the OTLP defaults apply.\n */\nexport type OtlpProviderOptions = {\n spanProcessors?: SpanProcessor[]\n logRecordProcessors?: LogRecordProcessor[]\n metricReaders?: IMetricReader[]\n instrumentations?: Instrumentation[]\n}\n\n/**\n * PII guard (telemetry spec Privacy / R12). The single source of truth for pg\n * instrumentation config: `enhancedDatabaseReporting: false` means bound SQL\n * parameter VALUES are NOT captured \u2014 only the statement shape ($1, $2). User\n * data lives in those params, so this must never be flipped on. Asserted by a\n * regression test.\n */\nexport const PG_INSTRUMENTATION_OPTIONS = { enhancedDatabaseReporting: false } as const\n\nexport class OtlpProvider implements TelemetryProvider {\n /** The configured backend name (signoz | newrelic | otlp) \u2014 all OTLP, vendor differs only by endpoint. */\n readonly name: string\n readonly supports: readonly TelemetrySignal[] = ['traces', 'metrics', 'logs', 'errors']\n\n private sdk: NodeSDK | undefined\n private readonly instruments = new Map<string, Counter | Histogram | Gauge>()\n\n constructor(private readonly options: OtlpProviderOptions = {}, name = 'otlp') {\n this.name = name\n }\n\n async start(): Promise<void> {\n if (this.sdk) return\n const env = readTelemetryEnv()\n\n this.sdk = new NodeSDK({\n resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: env.serviceName }),\n // Global propagation roots requests by default. Both standard and backup\n // inbound headers are honored only when TELEMETRY_TRUST_INBOUND_TRACE is\n // explicitly enabled behind trusted infrastructure.\n textMapPropagator: backupHeaderPropagator,\n // Parent-based so child spans follow the trace's sampling decision; root\n // spans sample at the configured ratio.\n sampler: new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(env.samplingRatio) }),\n spanProcessors: this.options.spanProcessors ?? [new BatchSpanProcessor(new OTLPTraceExporter())],\n logRecordProcessors: this.options.logRecordProcessors ?? [\n new BatchLogRecordProcessor({ exporter: new OTLPLogExporter() }),\n ],\n metricReaders: this.options.metricReaders ?? [\n new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter() }),\n ],\n instrumentations: this.options.instrumentations ?? [\n new PgInstrumentation(PG_INSTRUMENTATION_OPTIONS),\n new UndiciInstrumentation(),\n ],\n })\n this.sdk.start()\n }\n\n async shutdown(): Promise<void> {\n if (!this.sdk) return\n await this.sdk.shutdown()\n this.sdk = undefined\n }\n\n runInSpan<T>(name: string, options: SpanOptions, fn: (span: Span) => T): T {\n const tracer = trace.getTracer(TRACER_NAME)\n return tracer.startActiveSpan(name, toOtelSpanOptions(options), (otelSpan) =>\n runSpan(new OtelSpan(otelSpan), fn),\n )\n }\n\n activeSpan(): Span | undefined {\n const active = trace.getActiveSpan()\n return active ? new OtelSpan(active) : undefined\n }\n\n activeTraceContext(): TraceContext | undefined {\n const ctx = trace.getActiveSpan()?.spanContext()\n if (!ctx || !isSpanContextValid(ctx)) return undefined\n return { traceId: ctx.traceId, spanId: ctx.spanId }\n }\n\n inject(carrier: TraceCarrier): void {\n queuePropagator.inject(context.active(), carrier, defaultTextMapSetter)\n }\n\n runInRemoteSpan<T>(carrier: TraceCarrier, name: string, options: SpanOptions, fn: (span: Span) => T): T {\n const parent = queuePropagator.extract(context.active(), carrier, defaultTextMapGetter)\n const tracer = trace.getTracer(TRACER_NAME)\n return context.with(parent, () =>\n tracer.startActiveSpan(name, toOtelSpanOptions(options), (otelSpan) =>\n runSpan(new OtelSpan(otelSpan), fn),\n ),\n )\n }\n\n emitLog(record: LogRecord): void {\n const attributes: Record<string, AttributeValue> = cleanAttributes(record.attributes)\n if (record.error) {\n attributes['exception.type'] = redactPii(record.error.name)\n attributes['exception.message'] = redactPii(record.error.message)\n if (record.error.stack) attributes['exception.stacktrace'] = redactPii(record.error.stack)\n }\n logs.getLogger(TRACER_NAME).emit({\n severityNumber: SEVERITY[record.level],\n severityText: record.level,\n body: redactPii(record.message),\n attributes,\n })\n }\n\n recordMetric(point: MetricPoint): void {\n const labels = cleanAttributes(point.labels)\n const instrument = this.getInstrument(point)\n if (point.kind === 'counter') (instrument as Counter).add(point.value, labels)\n else (instrument as Histogram | Gauge).record(point.value, labels)\n }\n\n private getInstrument(point: MetricPoint): Counter | Histogram | Gauge {\n const key = `${point.kind}:${point.name}`\n let instrument = this.instruments.get(key)\n if (!instrument) {\n const meter = metrics.getMeter(TRACER_NAME)\n const opts = point.unit ? { unit: point.unit } : undefined\n instrument =\n point.kind === 'counter'\n ? meter.createCounter(point.name, opts)\n : point.kind === 'histogram'\n ? meter.createHistogram(point.name, opts)\n : meter.createGauge(point.name, opts)\n this.instruments.set(key, instrument)\n }\n return instrument\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,7 @@
1
+ const BACKUP_TRACEPARENT_HEADER = "x-original-traceparent";
2
+ const BACKUP_TRACESTATE_HEADER = "x-original-tracestate";
3
+ export {
4
+ BACKUP_TRACEPARENT_HEADER,
5
+ BACKUP_TRACESTATE_HEADER
6
+ };
7
+ //# sourceMappingURL=trace-headers.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/trace-headers.ts"],
4
+ "sourcesContent": ["/**\n * Backup trace headers shared by the server propagator (`provider/otlp-provider.ts`)\n * and the browser RUM wiring (`browser/`). Kept in a dependency-free module so the\n * client bundle can import the names without pulling any server-only code.\n *\n * The \"backup header\" pattern: our own callers inject `x-original-traceparent`\n * alongside the standard `traceparent`, so the original context survives a proxy\n * that rewrites `traceparent` in between. The backup header is *not* itself a\n * trust signal \u2014 at an HTTP boundary it is caller-controlled exactly like the\n * standard one, so the server ignores both and starts a fresh root unless\n * `TELEMETRY_TRUST_INBOUND_TRACE=true`. That flag is the supported way to stitch\n * a browser span to its server span; see `browser/propagator.ts` and the\n * extractor in `provider/otlp-provider.ts`.\n */\nexport const BACKUP_TRACEPARENT_HEADER = 'x-original-traceparent'\nexport const BACKUP_TRACESTATE_HEADER = 'x-original-tracestate'\n"],
5
+ "mappings": "AAcO,MAAM,4BAA4B;AAClC,MAAM,2BAA2B;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@open-mercato/telemetry",
3
- "version": "0.8.1-develop.7275.1.f772b944d4",
3
+ "version": "0.8.1-develop.7295.1.d0e0e33014",
4
+ "license": "MIT",
4
5
  "description": "Vendor-neutral spans, metrics, and error reporting backed by a pluggable OTLP provider. Off by default; the telemetry runtime is loaded only when explicitly enabled.",
5
6
  "type": "module",
6
7
  "main": "./dist/index.js",
@@ -15,6 +16,14 @@
15
16
  "types": "./src/index.ts",
16
17
  "default": "./dist/index.js"
17
18
  },
19
+ "./browser": {
20
+ "types": "./src/browser/index.ts",
21
+ "default": "./dist/browser/index.js"
22
+ },
23
+ "./browser/server": {
24
+ "types": "./src/browser/server.ts",
25
+ "default": "./dist/browser/server.js"
26
+ },
18
27
  "./*": {
19
28
  "types": [
20
29
  "./src/*.ts"
@@ -29,7 +38,15 @@
29
38
  }
30
39
  },
31
40
  "dependencies": {
32
- "@open-mercato/shared": "0.8.1-develop.7275.1.f772b944d4"
41
+ "@open-mercato/shared": "0.8.1-develop.7295.1.d0e0e33014"
42
+ },
43
+ "peerDependencies": {
44
+ "react": ">=18.0.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "react": {
48
+ "optional": true
49
+ }
33
50
  },
34
51
  "optionalDependencies": {
35
52
  "@opentelemetry/api": "^1.9.1",
@@ -39,20 +56,29 @@
39
56
  "@opentelemetry/exporter-metrics-otlp-http": "^0.220.0",
40
57
  "@opentelemetry/exporter-trace-otlp-http": "^0.220.0",
41
58
  "@opentelemetry/instrumentation": "^0.220.0",
59
+ "@opentelemetry/instrumentation-document-load": "^0.65.0",
60
+ "@opentelemetry/instrumentation-fetch": "^0.220.0",
42
61
  "@opentelemetry/instrumentation-pg": "^0.71.0",
43
62
  "@opentelemetry/instrumentation-undici": "^0.29.0",
44
63
  "@opentelemetry/resources": "^2.9.0",
45
64
  "@opentelemetry/sdk-logs": "^0.220.0",
46
65
  "@opentelemetry/sdk-metrics": "^2.9.0",
47
66
  "@opentelemetry/sdk-node": "^0.220.0",
67
+ "@opentelemetry/sdk-trace-base": "^2.9.0",
48
68
  "@opentelemetry/sdk-trace-node": "^2.9.0",
69
+ "@opentelemetry/sdk-trace-web": "^2.9.0",
49
70
  "@opentelemetry/semantic-conventions": "^1.41.1"
50
71
  },
51
72
  "devDependencies": {
52
- "@opentelemetry/sdk-trace-base": "^2.9.0",
73
+ "@testing-library/dom": "^10.4.1",
74
+ "@testing-library/react": "^16.3.1",
53
75
  "@types/jest": "^30.0.0",
54
76
  "@types/node": "^26.4.0",
77
+ "@types/react": "^19.2.17",
55
78
  "jest": "^30.4.2",
79
+ "jest-environment-jsdom": "^30.4.1",
80
+ "react": "19.2.8",
81
+ "react-dom": "19.2.8",
56
82
  "ts-jest": "^29.4.12",
57
83
  "typescript": "7.0.2"
58
84
  },