From 4d4e2ec2563ea7ced569f05184b81d03bcb63989 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 26 May 2026 14:51:50 +0100 Subject: [PATCH] fix(qa): require genai otel model spans (#86920) --- docs/concepts/qa-e2e-automation.md | 7 +++++-- scripts/qa-otel-smoke.ts | 28 +++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/docs/concepts/qa-e2e-automation.md b/docs/concepts/qa-e2e-automation.md index 5622a3dd5c12..a0708f368401 100644 --- a/docs/concepts/qa-e2e-automation.md +++ b/docs/concepts/qa-e2e-automation.md @@ -93,8 +93,11 @@ That script starts a local OTLP/HTTP receiver, runs the `otel-trace-smoke` QA scenario with the `diagnostics-otel` plugin enabled, then asserts traces, metrics, and logs are exported. It decodes the exported protobuf trace spans and checks the release-critical shape: -`openclaw.run`, `openclaw.harness.run`, `openclaw.model.call`, -`openclaw.context.assembled`, and `openclaw.message.delivery` must be present; +`openclaw.run`, `openclaw.harness.run`, a latest GenAI semantic-convention +model-call span, `openclaw.context.assembled`, and `openclaw.message.delivery` +must be present. The smoke forces +`OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental`, so the model-call +span must use the `{gen_ai.operation.name} {gen_ai.request.model}` name; model calls must not export `StreamAbandoned` on successful turns; raw diagnostic IDs and `openclaw.content.*` attributes must stay out of the trace. The raw OTLP payloads must not contain the prompt sentinel, response sentinel, or QA session diff --git a/scripts/qa-otel-smoke.ts b/scripts/qa-otel-smoke.ts index 5505a848d41a..f9067ecfb0f9 100644 --- a/scripts/qa-otel-smoke.ts +++ b/scripts/qa-otel-smoke.ts @@ -89,7 +89,6 @@ const OTLP_SIGNAL_PATHS = new Map([ const REQUIRED_SPAN_NAMES = [ "openclaw.run", "openclaw.harness.run", - "openclaw.model.call", "openclaw.context.assembled", "openclaw.message.delivery", ] as const; @@ -911,6 +910,19 @@ function capturedValueKind(value: string | number | boolean | string[]): string return Array.isArray(value) ? "array" : typeof value; } +function isLatestGenAiModelCallSpan(span: CapturedSpan): boolean { + const operationName = span.attributes["gen_ai.operation.name"]; + const modelName = span.attributes["gen_ai.request.model"]; + if (typeof operationName !== "string" || typeof modelName !== "string") { + return false; + } + return ( + span.name === `${operationName} ${modelName}` && + typeof span.attributes["openclaw.provider"] === "string" && + typeof span.attributes["openclaw.model"] === "string" + ); +} + function assertSmoke(params: { childExitCode: number; disallowedBodyNeedles: string[]; @@ -951,6 +963,13 @@ function assertSmoke(params: { failures.push(`missing required span ${name}`); } } + const modelSpans = params.spans.filter(isLatestGenAiModelCallSpan); + if (modelSpans.length === 0) { + failures.push("missing required GenAI model-call span"); + } + if (spanNames.has("openclaw.model.call")) { + failures.push("legacy openclaw.model.call span exported with GenAI semconv opt-in"); + } const metricNames = new Set(params.metrics.map((metric) => metric.name)); for (const name of REQUIRED_METRIC_NAMES) { if (!metricNames.has(name)) { @@ -975,8 +994,10 @@ function assertSmoke(params: { if (contentKeys.length > 0) { failures.push(`content attributes exported with capture disabled: ${contentKeys.join(", ")}`); } + if (modelSpans.some((span) => Object.hasOwn(span.attributes, "gen_ai.system"))) { + failures.push("legacy gen_ai.system attribute exported on GenAI model-call span"); + } - const modelSpans = params.spans.filter((span) => span.name === "openclaw.model.call"); const modelErrorSpans = modelSpans.filter((span) => { const serialized = JSON.stringify(span.attributes); return ( @@ -985,9 +1006,6 @@ function assertSmoke(params: { serialized.includes("StreamAbandoned") ); }); - if (modelSpans.length === 0) { - failures.push("no openclaw.model.call span was exported"); - } if (modelErrorSpans.length > 0) { failures.push("successful QA run exported model-call error attributes"); }