From 0f6705d22b21728fbb53778b57a0a05d3c2924f1 Mon Sep 17 00:00:00 2001 From: laboef1900 Date: Wed, 7 Oct 2026 13:08:23 +0200 Subject: [PATCH 1/6] chore(deps): align root @opentelemetry/api mirror with backend (^1.9.1) The root dependencies block mirrors backend prod deps so npm hoists them, but the root range lagged at ^1.9.0, leaving a nested backend/node_modules/@opentelemetry/api@1.9.1 in the lockfile. Re-locked: the nested copy is gone and the hoisted copy is 1.9.1. --- package-lock.json | 17 ++++------------- package.json | 2 +- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5bd277035..a8688a8c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "packages/contracts" ], "dependencies": { - "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api": "^1.9.1", "@opentelemetry/auto-instrumentations-node": "^0.79.0", "@opentelemetry/sdk-metrics": "2.10.0", "@opentelemetry/sdk-node": "^0.221.0", @@ -115,15 +115,6 @@ "vitest": "^5.0.0" } }, - "backend/node_modules/@opentelemetry/api": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", - "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=8.0.0" - } - }, "backend/node_modules/@tiptap/extension-code-block": { "version": "3.30.5", "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.30.5.tgz", @@ -3936,9 +3927,9 @@ } }, "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "license": "Apache-2.0", "engines": { "node": ">=8.0.0" diff --git a/package.json b/package.json index 44b47fde0..a82ad6817 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "//deps-note": "These mirror backend prod deps (kept in sync with backend/package.json) so npm hoists them to the root node_modules. Without this, conflicting versions from root devDep chains push backend's copy down to backend/node_modules, where the runtime Docker stage — which copies only the root tree and flattens backend/dist to /app/dist — can't reach them. The OpenTelemetry runtime stack lives here for exactly that reason (#1117: the whole observability path silently no-ops in the shipped image when these nest). The smoke step in .github/workflows/docker-build.yml guards against this regressing silently.", "dependencies": { - "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api": "^1.9.1", "@opentelemetry/auto-instrumentations-node": "^0.79.0", "@opentelemetry/sdk-metrics": "2.10.0", "@opentelemetry/sdk-node": "^0.221.0", From af7f48835fe3410938393799661e00314f6fa058 Mon Sep 17 00:00:00 2001 From: laboef1900 Date: Wed, 7 Oct 2026 13:08:30 +0200 Subject: [PATCH 2/6] chore: raise engines.node to the floor the dependency tree enforces .npmrc sets engine-strict=true, and the locked tree already refuses older Node 22 releases: jsdom 28 and isomorphic-dompurify (backend prod deps) and eslint-visitor-keys require ^22.13.0; vitest 5, vite 8 and rolldown require >=22.12.0. Declaring >=22.0.0 promised installs that fail with EBADENGINE. Root goes to >=22.13.0; mcp-docs (own lockfile, floor 22.12.0 from vitest/vite/jsdom) to >=22.12.0. --- CONTRIBUTING.md | 2 +- mcp-docs/package-lock.json | 2 +- mcp-docs/package.json | 2 +- package-lock.json | 2 +- package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a609ed510..8b184033e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Thank you for your interest in contributing to Compendiq! This guide will help y ## Prerequisites -- **Node.js** >= 22.0.0 (24 recommended) +- **Node.js** >= 22.13.0 (24 recommended) - **Docker** + **Docker Compose** (for PostgreSQL and Redis) - **Git** diff --git a/mcp-docs/package-lock.json b/mcp-docs/package-lock.json index a0ddc04c3..a3d090ec2 100644 --- a/mcp-docs/package-lock.json +++ b/mcp-docs/package-lock.json @@ -28,7 +28,7 @@ "vitest": "^5.0.0" }, "engines": { - "node": ">=22.0.0" + "node": ">=22.12.0" } }, "node_modules/@acemir/cssom": { diff --git a/mcp-docs/package.json b/mcp-docs/package.json index 5e7e3bfc8..a6d138548 100644 --- a/mcp-docs/package.json +++ b/mcp-docs/package.json @@ -38,6 +38,6 @@ "qs": "^6.16.0" }, "engines": { - "node": ">=22.0.0" + "node": ">=22.12.0" } } diff --git a/package-lock.json b/package-lock.json index a8688a8c7..00df29bdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "vitest": "^5.0.0" }, "engines": { - "node": ">=22.0.0" + "node": ">=22.13.0" } }, "backend": { diff --git a/package.json b/package.json index a82ad6817..ec7caece8 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,6 @@ "katex": "^0.18.2" }, "engines": { - "node": ">=22.0.0" + "node": ">=22.13.0" } } From 2ba6850927960f70fbf7be1a751c0815ab9de061 Mon Sep 17 00:00:00 2001 From: laboef1900 Date: Wed, 7 Oct 2026 13:10:30 +0200 Subject: [PATCH 3/6] test(deps): flag the katex/postcss-selector-parser overrides once parents widen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The root overrides for katex (mermaid wants ^0.16) and postcss-selector-parser (@tailwindcss/typography pins 6.0.10) exist only because the parents' own ranges exclude every patched release. Nothing noticed when that stops being true, at which point the override is dead weight or forces a downgrade. The invariants test now reads each parent's spec from the lockfile and fails with 'mermaid widened its katex range to X — drop the override' once it admits a patched version. Also renames the sharp floor test to say what it checks (sharp >= 0.35.5, not the bundled librsvg version). --- .../dependency-security-invariants.test.ts | 147 +++++++++++++++++- 1 file changed, 145 insertions(+), 2 deletions(-) diff --git a/backend/src/dependency-security-invariants.test.ts b/backend/src/dependency-security-invariants.test.ts index 57cd37ff6..4e44283a0 100644 --- a/backend/src/dependency-security-invariants.test.ts +++ b/backend/src/dependency-security-invariants.test.ts @@ -11,7 +11,10 @@ import { describe, expect, it } from 'vitest'; */ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); -type LockPackages = Record; +type LockPackages = Record< + string, + { version?: string; dependencies?: Record } | undefined +>; function lockPackages(): LockPackages { return JSON.parse(readFileSync(join(repoRoot, 'package-lock.json'), 'utf8')) @@ -40,12 +43,96 @@ function belowFloor(name: string, floor: string): string[] { return versions.filter((version) => !versionAtLeast(version, floor)); } +type Version = [number, number, number]; +type UpperBound = { version: Version; inclusive: boolean }; + +function compareVersions(a: Version, b: Version): number { + for (let i = 0; i < 3; i++) { + if (a[i] !== b[i]) return a[i]! - b[i]!; + } + return 0; +} + +/** `1.2.3` → given 3; `1.2` / `1.2.x` → given 2; `*` / `x` → given 0. */ +function parsePartial(text: string): { version: Version; given: number } { + const parts = text.split('-')[0]!.split('.'); + const numbers: number[] = []; + for (const part of parts) { + if (part === '' || /^[xX*]$/.test(part)) break; + if (!/^\d+$/.test(part)) throw new Error(`unsupported version "${text}"`); + numbers.push(Number(part)); + } + const given = numbers.length; + while (numbers.length < 3) numbers.push(0); + return { version: numbers.slice(0, 3) as Version, given: Math.min(given, 3) }; +} + +function bump(version: Version, index: number): Version { + return version.map((part, i) => (i < index ? part : i === index ? part + 1 : 0)) as Version; +} + +/** Lower and upper bound of one comparator (`^1.2.3`, `~1.2`, `>=1`, `1.2.x`, …). */ +function comparatorBounds(comparator: string): { lower?: Version; upper?: UpperBound } { + const [, operator = '', operand = ''] = comparator.match(/^(\^|~|>=|<=|>|<|=)?v?(.*)$/)!; + const { version, given } = parsePartial(operand); + if (given === 0) { + if (operator === '' || operator === '=' || operator === '>=') return {}; + throw new Error(`unsupported comparator "${comparator}"`); + } + switch (operator) { + case '>=': + case '>': + return { lower: version }; + case '<': + return { upper: { version, inclusive: false } }; + case '<=': + return { upper: { version, inclusive: true } }; + case '^': { + const firstNonZero = version.slice(0, given).findIndex((part) => part !== 0); + const index = firstNonZero === -1 ? given - 1 : firstNonZero; + return { lower: version, upper: { version: bump(version, index), inclusive: false } }; + } + case '~': + return { lower: version, upper: { version: bump(version, given >= 2 ? 1 : 0), inclusive: false } }; + default: + return given === 3 + ? { lower: version, upper: { version, inclusive: true } } + : { lower: version, upper: { version: bump(version, given - 1), inclusive: false } }; + } +} + +/** + * Whether `spec` admits any version at or above `floor`. Covers the range + * forms parents publish (exact, `^`, `~`, x-ranges, comparator sets, `||` + * unions); anything else throws, so an unparseable spec fails loudly instead + * of silently disabling the override expiry check below. A `>X` lower bound + * is treated as `>=X`, which only differs when the upper bound is also X. + */ +function rangeAdmitsAtLeast(spec: string, floor: string): boolean { + const minimum = parsePartial(floor).version; + return spec.split('||').some((alternative) => { + if (/\s-\s/.test(alternative)) throw new Error(`unsupported hyphen range "${spec}"`); + let lower: Version = minimum; + let upper: UpperBound | undefined; + for (const comparator of alternative.trim().split(/\s+/).filter(Boolean)) { + const bounds = comparatorBounds(comparator); + if (bounds.lower && compareVersions(bounds.lower, lower) > 0) lower = bounds.lower; + if (bounds.upper && (!upper || compareVersions(bounds.upper.version, upper.version) < 0)) { + upper = bounds.upper; + } + } + if (!upper) return true; + const gap = compareVersions(lower, upper.version); + return gap < 0 || (gap === 0 && upper.inclusive); + }); +} + describe('npm advisory floors', () => { it('resolves smol-toml past the quadratic parseKey scan (GHSA-r4xh-jqrq-34v2)', () => { expect(belowFloor('smol-toml', '1.9.0')).toEqual([]); }); - it('resolves sharp to a build bundling librsvg 2.63.2 (GHSA-wq5f-xc86-pv6w)', () => { + it('resolves sharp past the bundled-librsvg advisory (GHSA-wq5f-xc86-pv6w)', () => { expect(belowFloor('sharp', '0.35.5')).toEqual([]); }); @@ -65,3 +152,59 @@ describe('npm advisory floors', () => { expect(installed(lockPackages(), 'extract-zip')).toEqual([]); }); }); + +/** + * Root overrides that exist only because the parent's own range excludes every + * patched release (see `//tooling-deps-note` in the root package.json). Once + * the parent widens that range the override is dead weight — and can force a + * downgrade below what the parent now asks for — so it has to be dropped. + */ +const parentBlockedOverrides = [ + { dep: 'katex', parent: 'mermaid', patched: '0.18.2' }, + { dep: 'postcss-selector-parser', parent: '@tailwindcss/typography', patched: '7.1.6' }, +]; + +describe('root overrides waiting on a parent range', () => { + const rootManifest: { overrides?: Record } = JSON.parse( + readFileSync(join(repoRoot, 'package.json'), 'utf8'), + ); + const overrides = rootManifest.overrides ?? {}; + + it.each(parentBlockedOverrides)( + '$dep is overridden only while $parent still excludes a patched release', + ({ dep, parent, patched }) => { + expect(overrides, `the ${dep} override is gone — remove its row here`).toHaveProperty(dep); + const spec = lockPackages()[`node_modules/${parent}`]?.dependencies?.[dep]; + expect(spec, `${parent} no longer depends on ${dep} — drop the override in package.json`) + .toBeTypeOf('string'); + expect( + rangeAdmitsAtLeast(spec!, patched), + `${parent} widened its ${dep} range to ${spec} — drop the override in package.json`, + ).toBe(false); + }, + ); + + it.each([ + ['^0.16.47', '0.18.2', false], + ['^0.18.0', '0.18.2', true], + ['^0.19.0', '0.18.2', true], + ['~0.18.1', '0.18.2', true], + ['0.18.x', '0.18.2', true], + ['^0.0.3', '0.0.4', false], + ['6.0.10', '7.1.6', false], + ['7.1.6', '7.1.6', true], + ['^6.0.10', '7.1.6', false], + ['^7.0.0', '7.1.6', true], + ['>=6.0.0 <7.1.6', '7.1.6', false], + ['>=6.0.0 <=7.1.6', '7.1.6', true], + ['^6.0.10 || ^7.0.0', '7.1.6', true], + ['*', '7.1.6', true], + ] as const)('range %s admits a version >= %s: %s', (spec, floor, admits) => { + expect(rangeAdmitsAtLeast(spec, floor)).toBe(admits); + }); + + it('refuses range forms it cannot evaluate', () => { + expect(() => rangeAdmitsAtLeast('6.0.0 - 7.2.0', '7.1.6')).toThrow(/unsupported/); + expect(() => rangeAdmitsAtLeast('github:user/repo', '7.1.6')).toThrow(/unsupported/); + }); +}); From b03406ba253e1cacdee89535e766d60651ee4c9d Mon Sep 17 00:00:00 2001 From: laboef1900 Date: Wed, 7 Oct 2026 13:19:27 +0200 Subject: [PATCH 4/6] test(telemetry): pg spans must not carry the database username Regression guard for GHSA-qqmp-wf37-98f9. A child process registers the real auto-instrumentations (configured as startTelemetry() does) with an in-memory exporter before pg is loaded, runs a query against the test database, and asserts the pg.query span exists while no span carries db.user or the connection's username. Fails on instrumentation-pg 0.72.0 (db.user on pg.connect and pg.query), passes on 0.73.0. --- backend/src/telemetry.integration.test.ts | 83 +++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 backend/src/telemetry.integration.test.ts diff --git a/backend/src/telemetry.integration.test.ts b/backend/src/telemetry.integration.test.ts new file mode 100644 index 000000000..0319a03c2 --- /dev/null +++ b/backend/src/telemetry.integration.test.ts @@ -0,0 +1,83 @@ +import { execFile } from 'node:child_process'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; +import { describe, expect, it } from 'vitest'; +import { isDbAvailable } from './test-db-helper.js'; + +/** + * Database spans must not leak the DB username (GHSA-qqmp-wf37-98f9: + * `@opentelemetry/instrumentation-pg` ≤ 0.72 stamped every span with + * `db.user`). Exercised end to end: the real auto-instrumentations, as + * `startTelemetry()` configures them, record a real query against the test + * database into an in-memory exporter. + * + * Runs in a child process because instrumentation only attaches if it is + * registered before `pg` is first loaded — production guarantees that with the + * `--import ./dist/telemetry-register.js` preload, and this vitest worker has + * already loaded `pg` through test-setup. + */ +const dbAvailable = await isDbAvailable(); +const backendDir = join(dirname(fileURLToPath(import.meta.url)), '..'); + +const PROBE = ` +import { NodeSDK, tracing } from '@opentelemetry/sdk-node'; +import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; + +const exporter = new tracing.InMemorySpanExporter(); +const processor = new tracing.SimpleSpanProcessor(exporter); +const sdk = new NodeSDK({ + serviceName: 'pg-span-probe', + spanProcessors: [processor], + instrumentations: [ + getNodeAutoInstrumentations({ '@opentelemetry/instrumentation-fs': { enabled: false } }), + ], +}); +sdk.start(); + +// Dynamic on purpose: a static import would load pg before sdk.start(). +const { default: pg } = await import('pg'); +const client = new pg.Client({ connectionString: process.env.PROBE_POSTGRES_URL }); +await client.connect(); +await client.query('SELECT 1'); +await client.end(); + +// Spans wait for the async resource detectors before they reach the exporter; +// reading without a flush races them and intermittently sees no spans at all. +await processor.forceFlush(); +const spans = exporter.getFinishedSpans().map((span) => ({ name: span.name, attributes: span.attributes })); +await sdk.shutdown(); +process.stdout.write(JSON.stringify(spans)); +`; + +interface ExportedSpan { + name: string; + attributes: Record; +} + +describe.skipIf(!dbAvailable)('OpenTelemetry pg instrumentation', () => { + it('records pg query spans without the database username', async () => { + const postgresUrl = process.env.POSTGRES_URL!; + const username = decodeURIComponent(new URL(postgresUrl).username); + expect(username).not.toBe(''); + + const { stdout } = await promisify(execFile)( + process.execPath, + ['--input-type=module', '-e', PROBE], + { cwd: backendDir, env: { PATH: process.env.PATH, PROBE_POSTGRES_URL: postgresUrl } }, + ); + const spans: ExportedSpan[] = JSON.parse(stdout); + + const querySpan = spans.find((span) => span.name.startsWith('pg.query:')); + expect(querySpan, `no pg.query span among ${spans.map((span) => span.name).join(', ')}`) + .toBeDefined(); + + const leaks = spans.flatMap((span) => + Object.entries(span.attributes) + .filter(([key, value]) => key === 'db.user' || value === username) + .map(([key]) => `${span.name} ${key}`), + ); + expect(leaks).toEqual([]); + expect(querySpan!.attributes['db.system.name']).toBe('postgresql'); + }, 60_000); +}); From faead0c373a38d36725d80a41c16b71411178271 Mon Sep 17 00:00:00 2001 From: laboef1900 Date: Wed, 7 Oct 2026 13:20:51 +0200 Subject: [PATCH 5/6] docs(telemetry): document stable-semconv renames; drop Fastify instrumentation claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADMIN-GUIDE → OpenTelemetry now lists the span-attribute renames and the HTTP metric rename + unit change from the 0.221 OTel release (http.server.duration ms → http.server.request.duration s, same for client). Code comments and PERFORMANCE.md claimed Fastify auto-instrumentation, but neither @opentelemetry/instrumentation-fastify nor @fastify/otel is installed; Fastify requests are HTTP server spans. --- backend/Dockerfile | 2 +- backend/src/telemetry-register.test.ts | 2 +- backend/src/telemetry-register.ts | 2 +- backend/src/telemetry.ts | 5 +++-- docs/ADMIN-GUIDE.md | 27 ++++++++++++++++++++++++++ docs/PERFORMANCE.md | 8 ++++++-- 6 files changed, 39 insertions(+), 7 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 2453f2818..24431a47f 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -88,5 +88,5 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ ENTRYPOINT ["dumb-init", "--"] # `--import` preloads the OTel SDK before the app module graph loads so -# auto-instrumentation attaches to http/fastify/pg/redis (issue #922). +# auto-instrumentation attaches to http/undici/pg/redis (issue #922). CMD ["node", "--import", "./dist/telemetry-register.js", "dist/index.js"] diff --git a/backend/src/telemetry-register.test.ts b/backend/src/telemetry-register.test.ts index 97b8a61f9..219b97197 100644 --- a/backend/src/telemetry-register.test.ts +++ b/backend/src/telemetry-register.test.ts @@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url'; /** * OpenTelemetry preload invariants (issue #922). * - * Auto-instrumentation can only monkey-patch http/fastify/pg/redis if the SDK + * Auto-instrumentation can only monkey-patch http/undici/pg/redis if the SDK * starts BEFORE those modules are first imported. Starting it from inside * index.ts (after the app module graph has already been evaluated) is too late * and the instrumentations silently never attach. The fix runs the SDK from a diff --git a/backend/src/telemetry-register.ts b/backend/src/telemetry-register.ts index a8be3b513..a818ee5a2 100644 --- a/backend/src/telemetry-register.ts +++ b/backend/src/telemetry-register.ts @@ -4,7 +4,7 @@ * Launched via Node's `--import` hook BEFORE the application module graph * (`dist/index.js`) is evaluated — see the runtime `CMD` in backend/Dockerfile * and the `start` script in backend/package.json. This is what lets - * auto-instrumentation monkey-patch http/fastify/pg/redis before those modules + * auto-instrumentation monkey-patch http/undici/pg/redis before those modules * are first imported. Starting the SDK from inside index.ts (as the old code * did) ran too late: the module graph had already been loaded, so the * instrumentations never attached. diff --git a/backend/src/telemetry.ts b/backend/src/telemetry.ts index 7efce8574..3b5e21e1c 100644 --- a/backend/src/telemetry.ts +++ b/backend/src/telemetry.ts @@ -3,7 +3,7 @@ * * The SDK is started from `telemetry-register.ts`, which is loaded via Node's * `--import` preload hook BEFORE the application module graph — that ordering - * is what lets auto-instrumentation monkey-patch http/fastify/pg/redis before + * is what lets auto-instrumentation monkey-patch http/undici/pg/redis before * those modules are first imported (issue #922). The started SDK and tracer are * stashed on `globalThis` so `getTracer`/`shutdownTelemetry` can reach them * regardless of which entrypoint started the SDK. @@ -85,7 +85,8 @@ export async function startTelemetry(): Promise { serviceName, instrumentations: [ getNodeAutoInstrumentations({ - // Instrument HTTP, Fastify, pg, Redis automatically + // HTTP, undici, pg and Redis are instrumented automatically. No Fastify + // instrumentation is installed: requests appear as HTTP server spans. '@opentelemetry/instrumentation-fs': { enabled: false }, // too noisy }), ], diff --git a/docs/ADMIN-GUIDE.md b/docs/ADMIN-GUIDE.md index 04a7b4544..80d3e1500 100644 --- a/docs/ADMIN-GUIDE.md +++ b/docs/ADMIN-GUIDE.md @@ -1943,6 +1943,33 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://your-collector:4318 This exports traces to any OTLP-compatible collector (Jaeger, Grafana Tempo, Datadog, etc.). +The backend auto-instruments HTTP (including incoming Fastify requests, as HTTP +server spans), undici, `pg` and Redis. There is no Fastify-specific route +instrumentation. + +#### Stable semantic conventions (since the 0.221 OTel release) + +The OpenTelemetry instrumentations now emit **only** the stable semantic +conventions; there is no opt-out back to the old names. Collector rules, +dashboards and alerts keyed on the old names stop matching after the upgrade: + +| Old | New | +|-----|-----| +| span attribute `db.system` | `db.system.name` | +| span attribute `db.statement` | `db.query.text` | +| span attribute `db.name` | `db.namespace` | +| span attribute `http.method` | `http.request.method` | +| span attribute `http.status_code` | `http.response.status_code` | +| span attributes `net.peer.name` / `net.peer.port` | `server.address` / `server.port` | +| metric `http.server.duration` (histogram, **ms**) | `http.server.request.duration` (histogram, **s**) | +| metric `http.client.duration` (histogram, **ms**) | `http.client.request.duration` (histogram, **s**) | + +The HTTP duration metrics changed **unit** as well as name: renaming a query +is not enough — latency thresholds and panel units must move from +milliseconds to seconds (a 500 ms alert threshold becomes `0.5`), otherwise +they are off by a factor of 1000. Database spans no longer carry `db.user` +or `db.connection_string` (GHSA-qqmp-wf37-98f9). + ### Audit Logging Compendiq logs user actions and system events. Query audit logs via the admin API: `GET /api/admin/audit-log` (admin-authenticated, supports pagination/filtering). Logs are stored in PostgreSQL and include: diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index c878d22c7..6b0ad2667 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -218,8 +218,12 @@ npm run dev # View traces at http://localhost:16686 ``` -In addition to the HTTP/Fastify/pg/Redis auto-instrumentation, the backend -emits custom spans around every outbound LLM call +The HTTP/undici/pg/Redis auto-instrumentation (Fastify requests appear as HTTP +server spans; there is no Fastify route instrumentation) follows the stable +semantic conventions — e.g. `http.server.request.duration` is in **seconds** — +see [ADMIN-GUIDE → OpenTelemetry](ADMIN-GUIDE.md#opentelemetry) for the +renamed attributes and metrics. In addition, the backend emits custom spans +around every outbound LLM call (`backend/src/domains/llm/services/openai-compatible-client.ts`): | Span | Covers | Attributes | From d015f44b34045d46bc4661c1f721305082dcdfe4 Mon Sep 17 00:00:00 2001 From: laboef1900 Date: Wed, 7 Oct 2026 13:20:51 +0200 Subject: [PATCH 6/6] docs: advisory IDs and metric rename in the changelog; current sharp in the runbook CHANGELOG [Unreleased]: add the GHSA IDs for the fast-copy and transitive updates (verified against the repo's Dependabot alerts), the HTTP metric rename/unit change, and the Node floor. client-inference runbook: sharp 0.35.5 with @img/sharp-libvips 1.3.4 (librsvg 2.63.2) and GHSA-wq5f-xc86-pv6w. --- CHANGELOG.md | 35 ++++++++++++++++++++++--------- docs/runbooks/client-inference.md | 8 ++++--- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ccdfeab3..3181c5462 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security -- **`fast-copy` 4.0.x → 4.1.2** (stack exhaustion on deeply nested values), - pulled in by the dev-only `pino-pretty` in the backend and `mcp-docs`. Not - shipped in production images. +- **`fast-copy` 4.0.x → 4.1.2** (GHSA-jggr-w7fw-pc2j: stack exhaustion on + deeply nested values), pulled in by the dev-only `pino-pretty` in the + backend and `mcp-docs`. Not shipped in production images. - **`@modelcontextprotocol/sdk` 1.30.0 → 1.31.0** in the backend (root lockfile) and the `mcp-docs` sidecar (GHSA-6qxp-vccf-f47h: the OAuth client @@ -29,14 +29,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 attribute names (`db.system`, `db.statement`, `db.name`, `http.method`, `http.status_code`, `net.peer.*`) must move to `db.system.name`, `db.query.text`, `db.namespace`, `http.request.method`, - `http.response.status_code`, `server.*`. + `http.response.status_code`, `server.*`. The HTTP duration histograms are + renamed **and change unit**: `http.server.duration` / `http.client.duration` + (milliseconds) become `http.server.request.duration` / + `http.client.request.duration` (seconds). Panels on the old names go empty, + and a plain rename leaves latency thresholds off by 1000× — divide them by + 1000. See *ADMIN-GUIDE → OpenTelemetry*. - **Transitive dependency security updates.** `katex` 0.16.47 → 0.18.10 - (lazy-loaded by mermaid for `$$…$$` diagram labels; prototype-pollution - `trust` bypass), `sharp` 0.35.4 → 0.35.5 (bundled librsvg 2.63.2), and the - build/dev-only `shell-quote` 1.10.0 → 1.12.0, `postcss-selector-parser` - 6.0.10 → 7.1.6 and `smol-toml` 1.8.0 → 1.9.0. `sprintf-js` 1.0.3 stays: no - patched release exists, and it is reachable only through mammoth's - command-line entry point, which the backend never loads. + (GHSA-238p-pmpm-9mq7: lazy-loaded by mermaid for `$$…$$` diagram labels; + prototype-pollution `trust` bypass), `sharp` 0.35.4 → 0.35.5 + (GHSA-wq5f-xc86-pv6w: bundled librsvg 2.63.2), and the build/dev-only + `shell-quote` 1.10.0 → 1.12.0 (GHSA-pqg4-j6r4-53mv), + `postcss-selector-parser` 6.0.10 → 7.1.6 (GHSA-rj75-hqrm-r3gf) and + `smol-toml` 1.8.0 → 1.9.0 (GHSA-r4xh-jqrq-34v2). `sprintf-js` 1.0.3 stays + (GHSA-hp3w-g68c-fv3c): no patched release exists, and it is reachable only + through mammoth's command-line entry point, which the backend never loads. + +### Changed + +- **Node.js floor is now 22.13.0** (`engines.node`; `mcp-docs` 22.12.0). With + `engine-strict=true` the locked tree already refused older Node 22 releases + (jsdom 28 and isomorphic-dompurify need ^22.13.0, vitest 5 and vite 8 need + `>=22.12.0`), so the old `>=22.0.0` promised installs that failed with + `EBADENGINE`. ## [0.9.0] - 2026-10-06 diff --git a/docs/runbooks/client-inference.md b/docs/runbooks/client-inference.md index b6ab6f6b7..dcb8b93aa 100644 --- a/docs/runbooks/client-inference.md +++ b/docs/runbooks/client-inference.md @@ -133,9 +133,11 @@ nginx grants `script-src 'wasm-unsafe-eval'` and `worker-src 'self'`. Transformers 4.3.0 admits the patched native dependencies without overrides. The lockfile resolves: -- `sharp` 0.35.4, including its platform binaries and libheif 1.23.2, - addresses [GHSA-f88m-g3jw-g9cj](https://github.com/advisories/GHSA-f88m-g3jw-g9cj) - and [GHSA-rgj7-g3m4-5g8c](https://github.com/advisories/GHSA-rgj7-g3m4-5g8c). +- `sharp` 0.35.5 with its `@img/sharp-libvips-*` 1.3.4 platform binaries + (librsvg 2.63.2, libheif 1.23.5) addresses + [GHSA-f88m-g3jw-g9cj](https://github.com/advisories/GHSA-f88m-g3jw-g9cj), + [GHSA-rgj7-g3m4-5g8c](https://github.com/advisories/GHSA-rgj7-g3m4-5g8c) + and [GHSA-wq5f-xc86-pv6w](https://github.com/advisories/GHSA-wq5f-xc86-pv6w). - `onnxruntime-node` 1.30.0 admits `adm-zip ^0.6.0`; the lockfile selects 0.6.1, addressing [GHSA-xcpc-8h2w-3j85](https://github.com/advisories/GHSA-xcpc-8h2w-3j85), [GHSA-vwc7-r8mq-g2x9](https://github.com/advisories/GHSA-vwc7-r8mq-g2x9)