Files
template-app/test/observability.test.ts

293 lines
13 KiB
TypeScript

// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
// Observability of the server processes (spec §9.5, plan item AT4.2): errors, traces and logs
// configured from the deployment contract, carrying the request's trace id, with secrets and tokens
// taken out before anything leaves the process, and a plain no-op with a clear log line when the
// deployment has no DSN. The SDK is the real @conusai-sdk/observe-node; only the network is a
// recording `fetch`.
import {
createRedactor,
FILTERED,
loadAppEnv,
observabilityConfig,
observabilityLine,
secretsOf,
startObservability,
} from "@app/config"
import { ids, withRequest } from "@conusai-sdk/observe-node"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
const TRACE_ID = "4bf92f3577b34da6a3ce929d0e0e4736"
const TRACEPARENT = `00-${TRACE_ID}-00f067aa0ba902b7-01`
const SECRET = "s3cr3t-session-key-0123456789abcdef"
const TOKEN = "dt_0123456789abcdef0123456789abcdef"
const base: Record<string, string> = {
NODE_ENV: "test",
CONUSAI_FAKE_PLATFORM: "1",
CONUSAI_API_URL: "http://127.0.0.1:4010/api",
CONUSAI_API_TOKEN: TOKEN,
CONUSAI_PROJECT_ID: "42",
CONUSAI_ORG_ID: "9",
POSTGRES_URL: "postgresql://app_app:hunter2hunter2@db:5432/app",
POSTGRES_READER_URL: "postgresql://app_reader:another-long-pass@db:5432/app",
AUTH_SESSION_SECRET: SECRET,
}
const appEnv = (extra: Record<string, string> = {}, service: "web" | "worker" = "web") =>
loadAppEnv(service, { ...base, ...extra })
describe("the observability settings", () => {
it("reads release, environment and service name from the deployment contract", () => {
const env = appEnv({
CONUSAI_DSN: "https://pub@errors.example.test/42",
CONUSAI_RELEASE: "2026.10.1",
CONUSAI_ENVIRONMENT: "preview",
OTEL_SERVICE_NAME: "acme-review",
OTEL_EXPORTER_OTLP_ENDPOINT: "https://otel.example.test",
})
expect(observabilityConfig("web", env.deployment, env.production)).toMatchObject({
errors: true,
otlp: true,
release: "2026.10.1",
environment: "preview",
serviceName: "acme-review",
})
})
it("falls back to OTEL_SERVICE_VERSION, then to `dev`, and names the run mode when the platform names no environment", () => {
const platformRelease = appEnv({ OTEL_SERVICE_VERSION: "abc1234" })
expect(observabilityConfig("worker", platformRelease.deployment, false)).toMatchObject({
release: "abc1234",
environment: "development",
serviceName: "app-worker",
})
expect(observabilityConfig("web", appEnv().deployment, true)).toMatchObject({
release: "dev",
environment: "production",
})
})
it("says in one line what a process without a DSN will not send", () => {
const env = appEnv()
const line = observabilityLine(observabilityConfig("web", env.deployment, false))
expect(line).toContain("errors off (CONUSAI_DSN is not set)")
expect(line).toContain("logs and traces off (OTEL_EXPORTER_OTLP_ENDPOINT is not set)")
})
})
describe("redaction", () => {
const redact = createRedactor(secretsOf({ ...base }))
it.each([
["MONGODB_URL", "mongodb://app:encoded%2Fpassword-long@db:27017/app", "encoded/password-long"],
["CONVEX_ADMIN_KEY", "opaque-convex-administrator-value", "opaque-convex-administrator-value"],
["CONVEX_SELF_HOSTED_ADMIN_KEY", "opaque-self-hosted-convex-admin-value", "opaque-self-hosted-convex-admin-value"],
])("removes %s credentials from profile driver errors", (name, value, credential) => {
const profileRedactor = createRedactor(secretsOf({ [name]: value }))
expect(profileRedactor.text(`driver rejected ${credential}`)).toBe(`driver rejected ${FILTERED}`)
expect(profileRedactor.deep({ exception: { value: `driver rejected ${credential}` } })).toEqual({
exception: { value: `driver rejected ${FILTERED}` },
})
})
it("takes the process's own secret values out of any string, wherever they appear", () => {
expect(redact.text(`session ${SECRET} and token ${TOKEN}`)).toBe(`session ${FILTERED} and token ${FILTERED}`)
// A connection string's password goes on its own, too, as the driver quotes it in an error.
expect(redact.text("password authentication failed: hunter2hunter2")).toBe(
`password authentication failed: ${FILTERED}`,
)
})
it.each([
["a bearer credential", "sent Authorization: Bearer abcdef0123456789xyz", "abcdef0123456789xyz"],
["a credential in a URL", "connect postgresql://user:p4ssw0rd-long@db:5432/app", "p4ssw0rd-long"],
["a JSON web token", "id eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.c2lnbmF0dXJl", "eyJzdWIi"],
["an organization API key", "key cak_ab12cd34_0123456789abcdefghij", "0123456789abcdefghij"],
["an invitation link", "GET /invitations/acme/Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MA", "Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MA"],
[
"an invitation link inside a return_to query",
"GET /auth/sign-in?return_to=%2Finvitations%2Facme%2FZm9vYmFyYmF6cXV4MTIzNDU2Nzg5MA",
"Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MA",
],
["an authorization code in a query", "GET /auth/callback?code=abc123&state=xyz789", "abc123"],
["an e-mail address", "invited ada@example.test", "ada@example.test"],
])("removes %s", (_name, text, secret) => {
const cleaned = redact.text(text)
expect(cleaned).not.toContain(secret)
expect(cleaned.length).toBeGreaterThan(0)
})
it("keeps what is not a secret: the organization in an invitation link, the path, the words", () => {
expect(redact.text("GET /invitations/acme/Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MA failed")).toContain("/invitations/acme/")
expect(redact.text("procedure notes.note.create failed")).toBe("procedure notes.note.create failed")
})
it("filters a value under a key that names a secret, at any depth, and strings inside arrays", () => {
const cleaned = redact.deep({
headers: { authorization: "Bearer x", "x-csrf-token": "t", accept: "text/html" },
nested: { api_key: "k", session: { id: "s" }, list: [{ password: "p" }, "mail bob@example.test"] },
count: 3,
})
expect(cleaned).toEqual({
headers: { authorization: FILTERED, "x-csrf-token": FILTERED, accept: "text/html" },
nested: { api_key: FILTERED, session: FILTERED, list: [{ password: FILTERED }, "mail [email]"] },
count: 3,
})
})
it("survives a value that points at itself", () => {
const loop: Record<string, unknown> = { name: "loop" }
loop.self = loop
expect(redact.deep(loop)).toEqual({ name: "loop", self: FILTERED })
})
it("scrubs an error event but leaves the ids that join it to its request", () => {
const event = redact.event({
message: `failed for ${SECRET}`,
request: { url: "https://app.example.test/auth/callback?code=abc&state=def", headers: { cookie: "s=1" } },
contexts: {
trace: { trace_id: TRACE_ID, span_id: "00f067aa0ba902b7" },
conusai: { session_id: "8f6a1c0e-2b8d-4a43-9d6e-5d1f6c2b7a90", request_id: "req-1" },
runtime: { token: "x" },
},
exception: { values: [{ type: "Error", value: "bad Bearer abcdef0123456789xyz" }] },
})
expect(JSON.stringify(event)).not.toContain(SECRET)
expect(event.request?.url).toBe(`https://app.example.test/auth/callback?code=${FILTERED}&state=${FILTERED}`)
expect(event.request?.headers?.cookie).toBe(FILTERED)
expect(event.exception?.values?.[0]?.value).toBe(`bad Bearer ${FILTERED}`)
expect(event.contexts?.trace).toEqual({ trace_id: TRACE_ID, span_id: "00f067aa0ba902b7" })
expect(event.contexts?.conusai).toMatchObject({ request_id: "req-1" })
expect(event.contexts?.runtime).toEqual({ token: FILTERED })
})
})
describe("startObservability", () => {
const handlers = new Map<string, unknown[]>()
beforeEach(() => {
// Record the process handlers the SDK installs instead of installing them in the test worker.
vi.spyOn(process, "on").mockImplementation(((event: string, fn: unknown) => {
handlers.set(event, [...(handlers.get(event) ?? []), fn])
return process
}) as never)
vi.spyOn(process, "off").mockImplementation((() => process) as never)
})
afterEach(() => {
handlers.clear()
vi.restoreAllMocks()
})
it("is a no-op with one clear log line when the deployment has no DSN", async () => {
const lines: string[] = []
const observe = startObservability(appEnv(), { env: base, out: (line) => lines.push(line) })
expect(observe.errors).toBeUndefined()
expect(observe.otlp).toBeUndefined()
observe.captureException(new Error("nobody listens"))
await observe.shutdown()
expect(lines).toHaveLength(1)
expect(JSON.parse(lines[0] as string)).toMatchObject({
level: "info",
service: "app-web",
message: expect.stringContaining("errors off (CONUSAI_DSN is not set)"),
})
expect(handlers.size).toBe(0)
})
it("sends an error with the request's trace and request ids, and no secret", async () => {
const sent: Array<{ url: string; body: string }> = []
const recording = (async (url: string | URL | Request, init?: RequestInit) => {
const body = init?.body
const text = typeof body === "string" ? body : Buffer.from(body as Uint8Array).toString("utf8")
sent.push({ url: String(url), body: text })
return new Response("{}", { status: 200 })
}) as typeof fetch
const observe = startObservability(
appEnv({ CONUSAI_DSN: "https://pub123@errors.example.test/42", CONUSAI_RELEASE: "r-1" }),
{ env: base, out: () => undefined, fetch: recording },
)
expect(observe.errors).toBeDefined()
withRequest(new Headers({ traceparent: TRACEPARENT, "x-request-id": "req-77" }), () => {
observe.captureException(new Error(`db down at postgresql://app_app:hunter2hunter2@db:5432/app with ${TOKEN}`), {
procedure: "notes.note.create",
})
})
await observe.errors?.flush(2_000)
const [request] = sent
expect(request?.url).toContain("https://errors.example.test/api/42/envelope/")
expect(request?.body).toContain(TRACE_ID)
expect(request?.body).toContain("req-77")
expect(request?.body).toContain('"release":"r-1"')
expect(request?.body).not.toContain("hunter2hunter2")
expect(request?.body).not.toContain(TOKEN)
expect(request?.body).toContain(FILTERED)
await observe.errors?.close()
})
it("sends a log line over OTLP with the request's trace id, redacted, and prints it as a JSON line", async () => {
const posts: Array<{ url: string; body: string }> = []
const lines: string[] = []
const recording = (async (url: string | URL | Request, init?: RequestInit) => {
posts.push({ url: String(url), body: String(init?.body) })
return new Response("{}", { status: 200 })
}) as typeof fetch
const observe = startObservability(
appEnv({ OTEL_EXPORTER_OTLP_ENDPOINT: "http://127.0.0.1:4010/otel", OTEL_SERVICE_NAME: "acme-review" }),
{ env: base, out: (line) => lines.push(line), fetch: recording },
)
expect(observe.otlp?.enabled).toBe(true)
withRequest(new Headers({ traceparent: TRACEPARENT, "x-request-id": "req-78" }), () => {
expect(ids().trace_id).toBe(TRACE_ID)
observe.log.warn(`token ${TOKEN} refused`, { "conusai.org_slug": "acme", password: "p" })
})
await observe.otlp?.flush(2_000)
const logs = posts.filter((post) => post.url.endsWith("/otel/v1/logs"))
expect(logs.length).toBeGreaterThan(0)
const body = logs.map((post) => post.body).join("\n")
expect(body).toContain(TRACE_ID)
expect(body).toContain("acme-review")
expect(body).not.toContain(TOKEN)
expect(body).not.toContain('"p"')
const printed = lines
.map((line) => JSON.parse(line) as Record<string, string>)
.find((line) => line.level === "warn")
expect(printed).toMatchObject({ trace_id: TRACE_ID, request_id: "req-78", "conusai.org_slug": "acme" })
expect(JSON.stringify(printed)).not.toContain(TOKEN)
expect(printed?.password).toBe(FILTERED)
await observe.shutdown()
})
it("runs a function as a span under the request's trace and returns what it returns", async () => {
const posts: string[] = []
const recording = (async (url: string | URL | Request, init?: RequestInit) => {
if (String(url).endsWith("/otel/v1/traces")) posts.push(String(init?.body))
return new Response("{}", { status: 200 })
}) as typeof fetch
const observe = startObservability(appEnv({ OTEL_EXPORTER_OTLP_ENDPOINT: "http://127.0.0.1:4010/otel" }), {
env: base,
out: () => undefined,
fetch: recording,
})
const answer = withRequest(new Headers({ traceparent: TRACEPARENT }), () =>
observe.span("procedure notes.note.create", { "conusai.org_slug": "acme" }, () => 42),
)
expect(answer).toBe(42)
await observe.otlp?.flush(2_000)
const body = posts.join("\n")
expect(body).toContain("procedure notes.note.create")
expect(body).toContain(TRACE_ID)
expect(body).toContain("conusai.org_slug")
await observe.shutdown()
})
it("runs a function as itself when traces are off", () => {
const observe = startObservability(appEnv(), { env: base, out: () => undefined })
expect(observe.span("anything", {}, () => "plain")).toBe("plain")
})
})