From a9dc1e1cedb49a617cd18ded5aed20cde6112051 Mon Sep 17 00:00:00 2001 From: Andrii Kohut Date: Fri, 17 Jul 2026 12:30:17 +0200 Subject: [PATCH] feat: add otel test conventions and instrumentation sdk Define the OpenTelemetry semantic conventions for tests (span names, resource and case attributes, status mapping) as the canonical machine-readable source in the sdk, and build the instrumentation on top: an L1 fingerprint with os-agnostic path normalization and order-independent params hashing, a TestRunRecorder that accumulates a run and emits a contract-valid ingest batch, an emitRunSpans helper producing real OTel run and case spans with exception events, and a fail-open IngestClient with auth, idempotency headers and offline buffering. Document the conventions and the OTLP to contracts mapping in docs/otel-conventions.md. Closes #12. Closes #13. --- docs/otel-conventions.md | 64 +++++++++++ packages/sdk/package.json | 15 ++- packages/sdk/src/__tests__/sdk.test.ts | 150 +++++++++++++++++++++++++ packages/sdk/src/client.ts | 75 +++++++++++++ packages/sdk/src/conventions.ts | 32 ++++++ packages/sdk/src/fingerprint.ts | 29 +++++ packages/sdk/src/index.ts | 11 +- packages/sdk/src/recorder.ts | 115 +++++++++++++++++++ packages/sdk/src/spans.ts | 68 +++++++++++ pnpm-lock.yaml | 87 ++++++++++++++ 10 files changed, 638 insertions(+), 8 deletions(-) create mode 100644 docs/otel-conventions.md create mode 100644 packages/sdk/src/__tests__/sdk.test.ts create mode 100644 packages/sdk/src/client.ts create mode 100644 packages/sdk/src/conventions.ts create mode 100644 packages/sdk/src/fingerprint.ts create mode 100644 packages/sdk/src/recorder.ts create mode 100644 packages/sdk/src/spans.ts diff --git a/docs/otel-conventions.md b/docs/otel-conventions.md new file mode 100644 index 0000000..403cdcc --- /dev/null +++ b/docs/otel-conventions.md @@ -0,0 +1,64 @@ +# OpenTelemetry Test Conventions + +The canonical span and attribute model every reporter emits to. The machine-readable source of truth is [`packages/sdk/src/conventions.ts`](../packages/sdk/src/conventions.ts); this document is the human-readable spec. Product rationale lives in [ADR-0002](adr/0002-otel-native-ingestion.md) and the [wiki](https://github.com/AKogut/flakemetry/wiki/OTel-Test-Conventions). + +Conventions version: `0.1.0`. + +## Span hierarchy + +``` +test.run root span — one CI job / suite invocation + └─ test.case one execution of one test (retries are separate cases) + └─ test.step a step / hook (added in M2) +``` + +M1 emits `test.run` + `test.case`. `test.step` and network/browser child spans arrive in M2. + +## Resource / run attributes + +| Key | Example | Meaning | +|---|---|---| +| `service.name` | `flakemetry-reporter` | standard OTel service identity | +| `flakemetry.project` | `acme/web` | project the results belong to | +| `ci.provider` | `github_actions` | CI provider | +| `ci.run_id` | `9000001` | provider run id | +| `vcs.commit_sha` | `a1b2c3d` | commit under test — enables the same-sha flake signal | +| `vcs.branch` | `main` | branch | +| `vcs.pr_number` | `42` | pull request, when applicable | + +## Case span attributes + +| Key | Example | Meaning | +|---|---|---| +| `test.identity.fingerprint` | `sha256:…` | stable identity (L1), computed by the reporter | +| `test.suite` | `auth` | grouping | +| `test.title` | `logs in` | display name | +| `test.file_path` | `e2e/auth/login.spec.ts` | source location | +| `test.params_hash` | `9f2c…` | parameterized bucket, omitted when absent | +| `test.status` | `pass \| fail \| skip \| flaky` | verdict | +| `test.attempt` | `2` | retry index (1-based) | +| `test.duration_ms` | `1834` | wall-clock duration | + +## Status mapping + +| Test status | OTel span status | +|---|---| +| `pass`, `flaky` | `OK` | +| `fail` | `ERROR` (+ exception event carrying type / message / stack) | +| `skip` | `UNSET` | + +## Fingerprint (L1) + +`sha256(normalized_file_path + ' ' + suite + ' ' + title + ' ' + params_hash)` where the path is workspace-relative, POSIX-separated and lowercased. This is the exact-match layer; the server-side identity engine resolves moves and renames on top of it (#18). + +## OTLP → contracts mapping + +The SDK records executions and produces a contract-valid `ingestRunBatch` (`@flakemetry/contracts`) directly, so the same recorded run can be exported as OTLP spans or posted as the batch payload. Field mapping: + +| Batch field | Source | +|---|---| +| `resource.*` | run/resource attributes above | +| `executions[].{filePath,suite,title,status,attempt,durationMs}` | case span attributes | +| `executions[].retryOfIndex` | index of the earlier attempt in the same batch | +| `executions[].error` | the case span exception event | +| `idempotencyKey` | one per run; makes re-delivery safe | diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 7dc629e..c7c786b 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -32,11 +32,22 @@ "build": "tsup", "typecheck": "tsc -p tsconfig.json", "lint": "eslint .", - "clean": "rm -rf dist .turbo" + "clean": "rm -rf dist .turbo", + "test": "vitest run" }, "devDependencies": { "@flakemetry/eslint-config": "workspace:*", "@flakemetry/tsconfig": "workspace:*", - "@flakemetry/tsup-config": "workspace:*" + "@flakemetry/tsup-config": "workspace:*", + "@types/node": "^22.10.5", + "vitest": "^3.2.4" + }, + "dependencies": { + "@flakemetry/contracts": "workspace:*", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/sdk-trace-base": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.28.0" } } diff --git a/packages/sdk/src/__tests__/sdk.test.ts b/packages/sdk/src/__tests__/sdk.test.ts new file mode 100644 index 0000000..d5f1f1a --- /dev/null +++ b/packages/sdk/src/__tests__/sdk.test.ts @@ -0,0 +1,150 @@ +import { + BasicTracerProvider, + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/sdk-trace-base' +import { describe, expect, it } from 'vitest' + +import { IngestClient } from '../client' +import { RESOURCE_ATTR, SPAN_ATTR, SPAN_NAMES } from '../conventions' +import { computeFingerprint, hashParams, normalizeFilePath } from '../fingerprint' +import { type RunContext, TestRunRecorder } from '../recorder' +import { emitRunSpans } from '../spans' + +const context: RunContext = { + project: 'acme/web', + commitSha: 'a1b2c3d', + branch: 'main', + ciProvider: 'github_actions', + trigger: 'push', + ciRunId: '9000001', +} + +const makeRecorder = () => { + const recorder = new TestRunRecorder(context) + recorder.startRun(new Date('2026-07-16T10:00:00Z')) + recorder.record({ + filePath: 'e2e/auth/login.spec.ts', + suite: 'auth', + title: 'logs in', + status: 'fail', + attempt: 1, + startedAt: new Date('2026-07-16T10:00:01Z'), + durationMs: 1800, + error: { type: 'TimeoutError', message: 'Timeout 30000ms exceeded', stack: 'at login:12' }, + }) + recorder.record({ + filePath: 'e2e/auth/login.spec.ts', + suite: 'auth', + title: 'logs in', + status: 'flaky', + attempt: 2, + retryOfIndex: 0, + startedAt: new Date('2026-07-16T10:00:03Z'), + durationMs: 1400, + }) + recorder.finishRun('failed', new Date('2026-07-16T10:00:05Z')) + return recorder +} + +describe('fingerprint', () => { + it('normalizes paths so os and prefix differences do not fork identity', () => { + expect(normalizeFilePath('.\\E2E\\Auth\\Login.spec.ts')).toBe('e2e/auth/login.spec.ts') + }) + + it('is stable for the same test and distinct for a different title', () => { + const a = computeFingerprint({ filePath: 'a.spec.ts', suite: 's', title: 'does x' }) + const b = computeFingerprint({ filePath: 'a.spec.ts', suite: 's', title: 'does x' }) + const c = computeFingerprint({ filePath: 'a.spec.ts', suite: 's', title: 'does y' }) + expect(a).toBe(b) + expect(a).not.toBe(c) + expect(a.startsWith('sha256:')).toBe(true) + }) + + it('hashes params order-independently', () => { + expect(hashParams({ b: 2, a: 1 })).toBe(hashParams({ a: 1, b: 2 })) + expect(hashParams({})).toBeNull() + expect(hashParams(null)).toBeNull() + }) +}) + +describe('recorder to ingest batch', () => { + it('builds a contract-valid batch with computed fingerprints and retry linkage', () => { + const batch = makeRecorder().toIngestBatch('gh-9000001-1') + expect(batch.executions).toHaveLength(2) + expect(batch.resource.commitSha).toBe('a1b2c3d') + expect(batch.run.status).toBe('failed') + expect(batch.executions[1]?.retryOfIndex).toBe(0) + }) + + it('assigns the same identity to both attempts of the same test', () => { + const recorder = makeRecorder() + const [first, second] = recorder.recorded + expect(first?.fingerprint).toBe(second?.fingerprint) + }) +}) + +describe('otel spans', () => { + it('emits a run span parenting one case span per attempt with convention attributes', () => { + const exporter = new InMemorySpanExporter() + const provider = new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(exporter)], + }) + const tracer = provider.getTracer('test') + + emitRunSpans(tracer, makeRecorder()) + + const spans = exporter.getFinishedSpans() + const runSpan = spans.find((s) => s.name === SPAN_NAMES.run) + const caseSpans = spans.filter((s) => s.name === SPAN_NAMES.case) + + expect(runSpan).toBeDefined() + expect(caseSpans).toHaveLength(2) + expect(runSpan?.attributes[RESOURCE_ATTR.project]).toBe('acme/web') + expect(caseSpans[0]?.attributes[SPAN_ATTR.suite]).toBe('auth') + expect(caseSpans[0]?.attributes[SPAN_ATTR.fingerprint]).toContain('sha256:') + expect( + caseSpans.every((s) => s.parentSpanContext?.spanId === runSpan?.spanContext().spanId), + ).toBe(true) + expect(caseSpans[0]?.events[0]?.name).toBe('exception') + }) +}) + +describe('ingest client', () => { + it('sends the batch with auth and idempotency headers and parses the ack', async () => { + let seen: { url: string; headers: Record; body: string } | null = null + const fetchImpl = (async (url: string, init: RequestInit) => { + seen = { + url, + headers: init.headers as Record, + body: init.body as string, + } + return new Response(JSON.stringify({ receiptId: 'r1', acceptedExecutions: 2 }), { + status: 202, + }) + }) as unknown as typeof fetch + + const client = new IngestClient({ + endpoint: 'https://ingest.test/', + token: 'fmk_secret', + fetchImpl, + }) + const result = await client.send(makeRecorder().toIngestBatch('gh-9000001-1')) + + expect(result.ok).toBe(true) + expect(result.ack?.acceptedExecutions).toBe(2) + expect(seen!.url).toBe('https://ingest.test/v1/ingest') + expect(seen!.headers.authorization).toBe('Bearer fmk_secret') + expect(seen!.headers['idempotency-key']).toBe('gh-9000001-1') + }) + + it('fails open on a network error instead of throwing', async () => { + const fetchImpl = (async () => { + throw new Error('ECONNREFUSED') + }) as unknown as typeof fetch + const client = new IngestClient({ endpoint: 'https://ingest.test', token: 't', fetchImpl }) + const result = await client.send(makeRecorder().toIngestBatch('k12345678')) + expect(result.ok).toBe(false) + expect(result.error).toContain('ECONNREFUSED') + }) +}) diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts new file mode 100644 index 0000000..07a634e --- /dev/null +++ b/packages/sdk/src/client.ts @@ -0,0 +1,75 @@ +import { mkdirSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' + +import { type IngestAck, ingestAckSchema, type IngestRunBatch } from '@flakemetry/contracts' + +export interface IngestClientOptions { + endpoint: string + token: string + fetchImpl?: typeof fetch + bufferDir?: string | null + now?: () => number +} + +export interface IngestResult { + ok: boolean + status?: number + ack?: IngestAck + buffered?: boolean + error?: string +} + +export const INGEST_PATH = '/v1/ingest' + +export class IngestClient { + private readonly endpoint: string + private readonly token: string + private readonly fetchImpl: typeof fetch + private readonly bufferDir: string | null + private readonly now: () => number + + constructor(options: IngestClientOptions) { + this.endpoint = options.endpoint.replace(/\/+$/, '') + this.token = options.token + this.fetchImpl = options.fetchImpl ?? globalThis.fetch + this.bufferDir = options.bufferDir ?? null + this.now = options.now ?? (() => 0) + } + + private buffer(batch: IngestRunBatch): boolean { + if (!this.bufferDir) return false + try { + mkdirSync(this.bufferDir, { recursive: true }) + const file = join(this.bufferDir, `${batch.idempotencyKey}-${this.now()}.json`) + writeFileSync(file, JSON.stringify(batch)) + return true + } catch { + return false + } + } + + async send(batch: IngestRunBatch): Promise { + try { + const response = await this.fetchImpl(`${this.endpoint}${INGEST_PATH}`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + authorization: `Bearer ${this.token}`, + 'idempotency-key': batch.idempotencyKey, + }, + body: JSON.stringify(batch), + }) + + if (!response.ok) { + const buffered = this.buffer(batch) + return { ok: false, status: response.status, buffered } + } + + const ack = ingestAckSchema.parse(await response.json()) + return { ok: true, status: response.status, ack } + } catch (error) { + const buffered = this.buffer(batch) + return { ok: false, buffered, error: error instanceof Error ? error.message : String(error) } + } + } +} diff --git a/packages/sdk/src/conventions.ts b/packages/sdk/src/conventions.ts new file mode 100644 index 0000000..f9c61ef --- /dev/null +++ b/packages/sdk/src/conventions.ts @@ -0,0 +1,32 @@ +export const SPAN_NAMES = { + run: 'test.run', + case: 'test.case', + step: 'test.step', +} as const + +export const RESOURCE_ATTR = { + serviceName: 'service.name', + project: 'flakemetry.project', + ciProvider: 'ci.provider', + ciRunId: 'ci.run_id', + commitSha: 'vcs.commit_sha', + branch: 'vcs.branch', + prNumber: 'vcs.pr_number', +} as const + +export const SPAN_ATTR = { + fingerprint: 'test.identity.fingerprint', + suite: 'test.suite', + title: 'test.title', + paramsHash: 'test.params_hash', + status: 'test.status', + attempt: 'test.attempt', + retryOf: 'test.retry_of', + filePath: 'test.file_path', + durationMs: 'test.duration_ms', +} as const + +export const CONVENTIONS_VERSION = '0.1.0' + +export type ResourceAttributeKey = (typeof RESOURCE_ATTR)[keyof typeof RESOURCE_ATTR] +export type SpanAttributeKey = (typeof SPAN_ATTR)[keyof typeof SPAN_ATTR] diff --git a/packages/sdk/src/fingerprint.ts b/packages/sdk/src/fingerprint.ts new file mode 100644 index 0000000..f335515 --- /dev/null +++ b/packages/sdk/src/fingerprint.ts @@ -0,0 +1,29 @@ +import { createHash } from 'node:crypto' + +import type { JsonRecord } from '@flakemetry/contracts' + +export const normalizeFilePath = (filePath: string): string => + filePath.replaceAll('\\', '/').replace(/^\.\//, '').replace(/^\/+/, '').toLowerCase() + +export const hashParams = (params: JsonRecord | null | undefined): string | null => { + if (!params || Object.keys(params).length === 0) return null + const canonical = JSON.stringify(params, Object.keys(params).sort()) + return createHash('sha256').update(canonical).digest('hex').slice(0, 16) +} + +export interface FingerprintInput { + filePath: string + suite: string + title: string + paramsHash?: string | null +} + +export const computeFingerprint = (input: FingerprintInput): string => { + const parts = [ + normalizeFilePath(input.filePath), + input.suite.trim(), + input.title.trim(), + input.paramsHash ?? '', + ] + return `sha256:${createHash('sha256').update(parts.join(' ')).digest('hex')}` +} diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 073acfd..3903f35 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,6 +1,5 @@ -export const PACKAGE = '@flakemetry/sdk' as const - -export const sdk = { - name: PACKAGE, - version: '0.0.0', -} as const +export * from './client' +export * from './conventions' +export * from './fingerprint' +export * from './recorder' +export * from './spans' diff --git a/packages/sdk/src/recorder.ts b/packages/sdk/src/recorder.ts new file mode 100644 index 0000000..6b25e64 --- /dev/null +++ b/packages/sdk/src/recorder.ts @@ -0,0 +1,115 @@ +import { + type CiProvider, + type IngestExecution, + type IngestRunBatch, + ingestRunBatchSchema, + type JsonRecord, + type RunStatus, + type RunTrigger, + type TestStatus, +} from '@flakemetry/contracts' + +import { CONVENTIONS_VERSION } from './conventions' +import { computeFingerprint, hashParams } from './fingerprint' + +export interface RunContext { + project: string + commitSha: string + branch: string + ciProvider: CiProvider + trigger: RunTrigger + ciRunId?: string | null + prNumber?: number | null +} + +export interface RecordedTest { + filePath: string + suite: string + title: string + params?: JsonRecord | null + status: TestStatus + attempt?: number + retryOfIndex?: number | null + startedAt: Date + durationMs: number + error?: { type?: string | null; message: string; stack?: string | null } | null + attributes?: JsonRecord | null +} + +export interface RecordedTestWithIdentity extends RecordedTest { + fingerprint: string + paramsHash: string | null +} + +export class TestRunRecorder { + private readonly tests: RecordedTestWithIdentity[] = [] + private runStartedAt: Date | null = null + private runFinishedAt: Date | null = null + private runStatus: RunStatus = 'running' + + constructor(readonly context: RunContext) {} + + startRun(startedAt: Date): void { + this.runStartedAt = startedAt + } + + record(test: RecordedTest): RecordedTestWithIdentity { + const paramsHash = hashParams(test.params) + const fingerprint = computeFingerprint({ + filePath: test.filePath, + suite: test.suite, + title: test.title, + paramsHash, + }) + const enriched: RecordedTestWithIdentity = { ...test, fingerprint, paramsHash } + this.tests.push(enriched) + return enriched + } + + finishRun(status: RunStatus, finishedAt: Date): void { + this.runStatus = status + this.runFinishedAt = finishedAt + } + + get recorded(): readonly RecordedTestWithIdentity[] { + return this.tests + } + + private toExecution(test: RecordedTestWithIdentity): IngestExecution { + return { + filePath: test.filePath, + suite: test.suite, + title: test.title, + params: test.params ?? undefined, + status: test.status, + attempt: test.attempt ?? 1, + retryOfIndex: test.retryOfIndex ?? undefined, + startedAt: test.startedAt, + durationMs: test.durationMs, + error: test.error ?? undefined, + attributes: test.attributes ?? undefined, + } + } + + toIngestBatch(idempotencyKey: string): IngestRunBatch { + const startedAt = this.runStartedAt ?? this.tests[0]?.startedAt ?? new Date(0) + return ingestRunBatchSchema.parse({ + contractVersion: CONVENTIONS_VERSION, + idempotencyKey, + resource: { + ciProvider: this.context.ciProvider, + ciRunId: this.context.ciRunId ?? undefined, + commitSha: this.context.commitSha, + branch: this.context.branch, + prNumber: this.context.prNumber ?? undefined, + trigger: this.context.trigger, + }, + run: { + status: this.runStatus, + startedAt, + finishedAt: this.runFinishedAt ?? undefined, + }, + executions: this.tests.map((test) => this.toExecution(test)), + }) + } +} diff --git a/packages/sdk/src/spans.ts b/packages/sdk/src/spans.ts new file mode 100644 index 0000000..1446eb1 --- /dev/null +++ b/packages/sdk/src/spans.ts @@ -0,0 +1,68 @@ +import { context, SpanStatusCode, trace, type Tracer } from '@opentelemetry/api' + +import { RESOURCE_ATTR, SPAN_ATTR, SPAN_NAMES } from './conventions' +import type { TestRunRecorder } from './recorder' + +const spanStatusFor = (status: string): SpanStatusCode => { + if (status === 'fail') return SpanStatusCode.ERROR + if (status === 'skip') return SpanStatusCode.UNSET + return SpanStatusCode.OK +} + +export const emitRunSpans = (tracer: Tracer, recorder: TestRunRecorder): void => { + const { context: runContext, recorded } = recorder + const runStart = recorded[0]?.startedAt ?? new Date() + + const runSpan = tracer.startSpan(SPAN_NAMES.run, { + startTime: runStart, + attributes: { + [RESOURCE_ATTR.project]: runContext.project, + [RESOURCE_ATTR.commitSha]: runContext.commitSha, + [RESOURCE_ATTR.branch]: runContext.branch, + [RESOURCE_ATTR.ciProvider]: runContext.ciProvider, + ...(runContext.ciRunId ? { [RESOURCE_ATTR.ciRunId]: runContext.ciRunId } : {}), + ...(runContext.prNumber ? { [RESOURCE_ATTR.prNumber]: runContext.prNumber } : {}), + }, + }) + + const runCtx = trace.setSpan(context.active(), runSpan) + let lastEnd = runStart.getTime() + + for (const test of recorded) { + const endTime = new Date(test.startedAt.getTime() + test.durationMs) + lastEnd = Math.max(lastEnd, endTime.getTime()) + + const caseSpan = tracer.startSpan( + SPAN_NAMES.case, + { + startTime: test.startedAt, + attributes: { + [SPAN_ATTR.fingerprint]: test.fingerprint, + [SPAN_ATTR.suite]: test.suite, + [SPAN_ATTR.title]: test.title, + [SPAN_ATTR.filePath]: test.filePath, + [SPAN_ATTR.status]: test.status, + [SPAN_ATTR.attempt]: test.attempt ?? 1, + [SPAN_ATTR.durationMs]: test.durationMs, + ...(test.paramsHash ? { [SPAN_ATTR.paramsHash]: test.paramsHash } : {}), + }, + }, + runCtx, + ) + + if (test.error) { + caseSpan.recordException({ + name: test.error.type ?? 'Error', + message: test.error.message, + stack: test.error.stack ?? undefined, + }) + } + caseSpan.setStatus({ code: spanStatusFor(test.status) }) + caseSpan.end(endTime) + } + + runSpan.setStatus({ + code: recorded.some((t) => t.status === 'fail') ? SpanStatusCode.ERROR : SpanStatusCode.OK, + }) + runSpan.end(new Date(lastEnd)) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aaee256..33d5c98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,6 +195,25 @@ importers: version: link:../tsup-config packages/sdk: + dependencies: + '@flakemetry/contracts': + specifier: workspace:* + version: link:../contracts + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.1 + '@opentelemetry/core': + specifier: ^2.0.0 + version: 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': + specifier: ^2.0.0 + version: 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': + specifier: ^2.0.0 + version: 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': + specifier: ^1.28.0 + version: 1.43.0 devDependencies: '@flakemetry/eslint-config': specifier: workspace:* @@ -205,6 +224,12 @@ importers: '@flakemetry/tsup-config': specifier: workspace:* version: link:../tsup-config + '@types/node': + specifier: ^22.10.5 + version: 22.20.1 + vitest: + specifier: ^3.2.4 + version: 3.2.7(@types/node@22.20.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0) packages/tsconfig: {} @@ -685,6 +710,38 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.9.0': + resolution: {integrity: sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/resources@2.9.0': + resolution: {integrity: sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.9.0': + resolution: {integrity: sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace@2.9.0': + resolution: {integrity: sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.43.0': + resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} + engines: {node: '>=14'} + '@prisma/client@6.19.3': resolution: {integrity: sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==} engines: {node: '>=18.18'} @@ -2399,6 +2456,36 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/resources@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-trace@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/semantic-conventions@1.43.0': {} + '@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3)': optionalDependencies: prisma: 6.19.3(typescript@5.9.3)