.agents/skills/llmobs-testing/SKILL.md
That choice picks the response source and test setup — cassettes for a workflow record nothing, while pure-function tests for a provider-backed call miss the network surface entirely. The operation independently determines its span kind and fields. These are working categories for reasoning; none exists as a code constant.
fetch.fetch instead of a cassette — where the spec supplies the responses itself: google-cloud-vertexai
swaps global.fetch per test and stubs Google auth, openai-agents and some ai providers pass a fetch option
to the client they construct.See references/category-strategies.md for the forbidden-vs-required matrix per strategy.
LLMObs tests use special helpers to validate span events.
Key components:
useLlmObs() - Initializes LLMObs test environmentgetEvents() - Retrieves captured span eventsassertLlmObsSpanEvent() - Validates span structure with flexible matchersBasic test flow:
useLlmObs({ plugin: 'name' })getEvents()assertLlmObsSpanEvent()See references/test-structure.md for complete test file templates.
Provider traffic is recorded once and replayed afterwards. Clients reach the proxy at
http://127.0.0.1:9126/vcr/{provider}; the category block above decides which categories use it at all.
Two facts block every first run:
docker compose up -d testagent.
Without it every call fails with ECONNREFUSED 127.0.0.1:9126, which reads like a provider outage.packages/dd-trace/test/llmobs/cassettes/{provider}/, with
generated names, rather than beside the spec.See references/vcr-cassettes.md for recording, provider mapping, body normalizers, and the commands to run a single integration.
The block at the top maps response source to test strategy. The operation maps independently to a span kind:
http://127.0.0.1:9126/vcr/{provider} or a canned fetch. Chat and generation emit llm; LangChain and ai
also expose operations with other kinds.'workflow' or 'agent', never 'llm' — the orchestrator coordinates libraries that
call providers rather than calling them itself. Nodes return plain values, so the test exercises graph execution
instead of a provider API.See references/category-strategies.md for the patterns per shape.
assertLlmObsSpanEvent(actual, expected)
Validates span structure with flexible matchers for non-deterministic values.
Available matchers: each one is a typeof or nullish check, not a value check.
MOCK_STRING - any string, '' included (use for output text)MOCK_NOT_NULLISH - anything but null / undefined, so 0 and '' pass (use for token counts)MOCK_NUMBER - any numberMOCK_OBJECT - anything with typeof 'object', null included (opaque schema / metadata payloads, or
a whole output message whose shape varies, as the ai specs do)Required fields: span, spanKind, name, tags. A missing tags throws
TypeError: Cannot read properties of undefined (reading 'ml_app') instead of failing an assertion, and every
plugin span carries at least { ml_app: 'test', integration: '<integration>' }.
Optional fields: modelName, modelProvider, inputMessages, outputMessages, inputDocuments,
outputDocuments, inputValue, outputValue, metrics, metadata, toolDefinitions, error, parentId,
sessionId, traceId. Omitting a field asserts its absence rather than ignoring it: no model fields, no input, no
output, no metadata, no tool definitions, metrics of {}, status: 'ok', and the root parent id. traceId is
the exception: omission defaults to MOCK_STRING because every event has one. See
references/assertion-helpers.md for the patterns.
Location: packages/dd-trace/test/llmobs/plugins/{integration}/index.spec.js. One file per
major-version surface when the SDK's shape changed across majors, named after it rather than kept in
one file — openaiv3.spec.js / openaiv4.spec.js, index.spec.js / index.v7.spec.js.
Structure:
'../../util'useLlmObs() installs the tracer, then recreate mutable clients per testdescribe('chat completions', ...))Standard imports:
useLlmObs, assertLlmObsSpanEvent, MOCK_STRING, MOCK_NOT_NULLISH, MOCK_NUMBER, MOCK_OBJECT
See references/test-structure.md for complete template.
SPAN_KINDS in packages/dd-trace/src/llmobs/constants/tags.js is the list the public SDK validates against:
llm (chat / completions), workflow, agent, task (a unit of work inside a workflow), tool, embedding,
retrieval. Plugins set the kind directly and skip that validation, so kinds outside the list exist — ai v7
and claude-agent-sdk both emit step.
Pinning a field the kind never emits asserts metadata production does not produce:
llm — modelName, modelProvider, inputMessages / outputMessages, and any emitted token metrics /
metadataembedding — modelName, modelProvider, inputDocuments, outputValue, sometimes metricsretrieval — inputValue, outputDocumentsworkflow / agent / task / step / tool — kind-specific inputValue / outputValue, sometimes
metadata, never
model fields or token metricsCover every instrumented method, and a multi-turn conversation where the surface takes one.
On errors the span is still submitted. Match the plugin's output contract: OpenAI and GenAI carry
outputMessages: [{ content: '', role: '' }], while Anthropic and non-llm integrations may omit output.
Pass a truthy marker to expect an error:
error: {},
The option decides only whether the expected event carries status: 'error' — assertLlmObsSpanEvent
copies the three error fields out of the span it is checking, so the marker does not pin the throw.
A call that resolves still fails on that status. Pin which error was thrown on the APM span the LLMObs
span was built from:
assert.strictEqual(apmSpans[0].meta['error.message'], error.message)