Skip to content

fix(llmobs): render BaseMessage in langgraph workflow I/O - #8097

Merged
crysmags merged 4 commits into
masterfrom
fix/langgraph-llmobs
Apr 27, 2026
Merged

fix(llmobs): render BaseMessage in langgraph workflow I/O#8097
crysmags merged 4 commits into
masterfrom
fix/langgraph-llmobs

Conversation

@crysmags

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes #8096. The LangGraph LLM Obs plugin's workflow span now renders BaseMessage instances (HumanMessage, AIMessage, SystemMessage, …) as { content, role } objects, matching the LangChain plugin's child chat_model spans — instead of dumping the full class with additional_kwargs, response_metadata, id, tool_call_id, etc. into the Input/Output panels.

Motivation

The LangGraph plugin (added in #7567) shipped its own formatIO that only recursed plain Object / Array. Since BaseMessage subclasses have constructor.name of HumanMessage / AIMessage / etc., they fell through to JSON.stringify(data) and serialized the entire class shape. This made the workflow span emitted by createAgent(...).invoke(...) / Pregel.stream essentially unreadable, even though the child chat_model and tool spans rendered correctly.

What changed

  • Extracted ROLE_MAPPINGS, getRole, getContentFromMessage, and formatIO out of packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js into a new shared module at packages/dd-trace/src/llmobs/plugins/shared/messages.js.
  • Rewired the LangChain handler to delegate to the shared helpers (this.formatIO / this.getContentFromMessage / this.getRole preserved as thin wrappers so chain.js, chat_model.js, embedding.js, vectorstore.js keep working unchanged).
  • Rewired packages/dd-trace/src/llmobs/plugins/langgraph/index.js to use the shared formatIO, which now falls through to getContentFromMessage for non-plain-object values — handling BaseMessage the same way LangChain does.

Testing

  • New unit tests in packages/dd-trace/test/llmobs/plugins/shared/messages.spec.js covering getRole (explicit role, _getType, getType), getContentFromMessage (string passthrough, BaseMessage → { content, role }, missing content default), and formatIO (null / primitive / plain object / array / BaseMessage / nested LangGraph-shape state).
  • New regression test in packages/dd-trace/test/llmobs/plugins/langgraph/index.spec.js that drives a Pregel graph with real HumanMessage / AIMessage instances and asserts the workflow span's input.value / output.value render as { content, role }.
  • Unit tests pass locally (13/13). The existing langgraph plugin integration specs depend on a local test agent not present in my environment; they run in CI via the langgraph job in .github/workflows/llmobs.yml.

Additional Notes

This is a strictly additive refactor: the LangChain handler's observable behavior is unchanged aside from formatIO(null) / formatIO(undefined) / formatIO(primitive) at the root (previously crashed or returned { content: '' }, now returns '' or the primitive). Existing LangChain call sites always pass objects, so no test fixtures need to change.

🤖 Generated with Claude Code

The langgraph plugin had its own formatIO that only recursed plain
Object/Array, so BaseMessage instances (HumanMessage, AIMessage, …)
fell through to JSON.stringify and serialized the full class shape
(additional_kwargs, response_metadata, id, tool_call_id, …) into the
workflow span's Input/Output panels.

Extract the langchain handler's formatIO/getContentFromMessage/getRole
into a shared util under llmobs/plugins/shared/messages.js and use it
from both plugins, so workflow spans and child chat_model spans render
messages consistently as { content, role }.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@crysmags
crysmags requested a review from a team as a code owner April 24, 2026 16:44
@github-actions

github-actions Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Overall package size

Self size: 5.56 MB
Deduped: 6.4 MB
No deduping: 6.4 MB

Dependency sizes | name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.0.1 | 82.56 kB | 817.39 kB | | dc-polyfill | 0.1.10 | 26.73 kB | 26.73 kB |

🤖 This report was automatically generated by heaviest-objects-in-the-universe

@codecov

codecov Bot commented Apr 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.61538% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.79%. Comparing base (df1f326) to head (cfd4e5e).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
.../dd-trace/src/llmobs/plugins/langchain/messages.js 82.14% 5 Missing ⚠️
...src/llmobs/plugins/langchain/handlers/embedding.js 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #8097      +/-   ##
==========================================
+ Coverage   73.70%   73.79%   +0.08%     
==========================================
  Files         783      784       +1     
  Lines       36369    36369              
==========================================
+ Hits        26805    26837      +32     
+ Misses       9564     9532      -32     
Flag Coverage Δ
aiguard-macos 36.38% <ø> (-0.09%) ⬇️
aiguard-ubuntu 36.49% <ø> (-0.09%) ⬇️
aiguard-windows 36.29% <ø> (-0.09%) ⬇️
apm-capabilities-tracing-macos 48.17% <17.94%> (+0.02%) ⬆️
apm-capabilities-tracing-ubuntu-active 48.19% <17.94%> (+0.02%) ⬆️
apm-capabilities-tracing-ubuntu-latest 48.16% <17.94%> (+0.02%) ⬆️
apm-capabilities-tracing-ubuntu-maintenance 48.19% <17.94%> (+0.02%) ⬆️
apm-capabilities-tracing-ubuntu-oldest 48.18% <17.94%> (+0.02%) ⬆️
apm-capabilities-tracing-windows 47.99% <17.94%> (+0.01%) ⬆️
apm-integrations-child-process 36.03% <ø> (-0.10%) ⬇️
apm-integrations-couchbase-18 35.04% <ø> (-0.09%) ⬇️
apm-integrations-couchbase-eol 35.09% <ø> (-0.09%) ⬇️
apm-integrations-oracledb 35.10% <ø> (-0.09%) ⬇️
appsec-express 52.73% <ø> (-0.06%) ⬇️
appsec-fastify 49.21% <ø> (-0.07%) ⬇️
appsec-graphql 49.50% <ø> (-0.07%) ⬇️
appsec-kafka 41.98% <ø> (-0.07%) ⬇️
appsec-ldapjs 41.31% <ø> (-0.08%) ⬇️
appsec-lodash ?
appsec-macos 56.75% <ø> (-0.07%) ⬇️
appsec-mongodb-core 45.63% <ø> (-0.07%) ⬇️
appsec-mongoose 46.51% <ø> (-0.07%) ⬇️
appsec-mysql 48.69% <ø> (-0.07%) ⬇️
appsec-node-serialize 40.51% <ø> (-0.08%) ⬇️
appsec-passport 44.52% <ø> (-0.08%) ⬇️
appsec-postgres 48.40% <ø> (-0.05%) ⬇️
appsec-sourcing 40.01% <ø> (-0.08%) ⬇️
appsec-stripe 42.24% <ø> (-0.08%) ⬇️
appsec-template 40.68% <ø> (-0.08%) ⬇️
appsec-ubuntu 56.83% <ø> (-0.07%) ⬇️
appsec-windows 56.65% <ø> (-0.05%) ⬇️
instrumentations-instrumentation-bluebird 29.75% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-body-parser 37.64% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-child_process 35.41% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-cookie-parser 31.68% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-express 31.90% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-express-mongo-sanitize 31.80% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-express-session 37.27% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-fs 29.42% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-generic-pool 30.53% <ø> (ø)
instrumentations-instrumentation-http 36.87% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-knex 29.72% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-light-my-request 37.20% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-mongoose 30.82% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-multer 37.41% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-mysql2 35.38% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-passport 41.17% <ø> (?)
instrumentations-instrumentation-passport-http 40.87% <ø> (-0.08%) ⬇️
instrumentations-instrumentation-passport-local 41.38% <ø> (-0.08%) ⬇️
instrumentations-instrumentation-pg 34.91% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-promise 29.68% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-promise-js 29.69% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-q 29.72% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-url 29.68% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-when 29.70% <ø> (-0.10%) ⬇️
llmobs-ai 38.35% <ø> (-0.09%) ⬇️
llmobs-anthropic 37.82% <ø> (-0.09%) ⬇️
llmobs-bedrock 37.06% <ø> (-0.08%) ⬇️
llmobs-google-genai 37.49% <ø> (-0.08%) ⬇️
llmobs-langchain 36.99% <84.21%> (-0.05%) ⬇️
llmobs-openai 41.18% <ø> (-0.08%) ⬇️
llmobs-vertex-ai 37.67% <ø> (-0.09%) ⬇️
platform-core 30.03% <ø> (ø)
platform-esbuild 32.83% <ø> (ø)
platform-instrumentations-misc 40.01% <ø> (ø)
platform-shimmer 35.72% <ø> (ø)
platform-unit-guardrails 31.39% <ø> (ø)
platform-webpack 20.73% <ø> (ø)
plugins-azure-durable-functions 25.36% <ø> (ø)
plugins-azure-event-hubs 25.51% <ø> (ø)
plugins-azure-service-bus 24.92% <ø> (ø)
plugins-bullmq 40.67% <ø> (-0.09%) ⬇️
plugins-cassandra 35.24% <ø> (-0.09%) ⬇️
plugins-cookie 26.47% <ø> (ø)
plugins-cookie-parser 26.28% <ø> (ø)
plugins-crypto 25.70% <ø> (ø)
plugins-dd-trace-api 35.34% <ø> (-0.09%) ⬇️
plugins-express-mongo-sanitize 26.42% <ø> (ø)
plugins-express-session 26.24% <ø> (ø)
plugins-fastify 39.25% <ø> (-0.09%) ⬇️
plugins-fetch 35.75% <ø> (-0.09%) ⬇️
plugins-fs 35.63% <ø> (-0.10%) ⬇️
plugins-generic-pool 25.40% <ø> (ø)
plugins-google-cloud-pubsub 42.99% <ø> (-0.08%) ⬇️
plugins-grpc 38.01% <ø> (-0.09%) ⬇️
plugins-handlebars 26.46% <ø> (ø)
plugins-hapi 37.25% <ø> (-0.09%) ⬇️
plugins-hono 37.50% <ø> (-0.09%) ⬇️
plugins-ioredis 35.68% <ø> (?)
plugins-knex 26.14% <ø> (ø)
plugins-langgraph 35.03% <17.94%> (-0.04%) ⬇️
plugins-ldapjs 24.02% <ø> (ø)
plugins-light-my-request 25.88% <ø> (ø)
plugins-limitd-client 29.98% <ø> (?)
plugins-lodash 25.47% <ø> (ø)
plugins-mariadb 36.60% <ø> (-0.05%) ⬇️
plugins-memcached 35.33% <ø> (-0.09%) ⬇️
plugins-microgateway-core 36.34% <ø> (-0.09%) ⬇️
plugins-modelcontextprotocol-sdk 34.27% <ø> (-0.09%) ⬇️
plugins-moleculer 38.03% <ø> (-0.09%) ⬇️
plugins-mongodb 36.61% <ø> (+0.02%) ⬆️
plugins-mongodb-core 36.13% <ø> (-0.12%) ⬇️
plugins-mongoose 36.01% <ø> (-0.09%) ⬇️
plugins-multer 26.24% <ø> (ø)
plugins-mysql 36.28% <ø> (-0.09%) ⬇️
plugins-mysql2 36.40% <ø> (-0.09%) ⬇️
plugins-node-serialize 26.51% <ø> (ø)
plugins-opensearch 35.00% <ø> (-0.09%) ⬇️
plugins-passport-http 26.30% <ø> (ø)
plugins-pino 31.78% <ø> (-0.09%) ⬇️
plugins-postgres 34.39% <ø> (-0.11%) ⬇️
plugins-process 25.70% <ø> (ø)
plugins-pug 26.47% <ø> (ø)
plugins-redis 35.89% <ø> (-0.09%) ⬇️
plugins-router 39.90% <ø> (-0.09%) ⬇️
plugins-sequelize 25.18% <ø> (ø)
plugins-test-and-upstream-amqp10 35.65% <ø> (-0.09%) ⬇️
plugins-test-and-upstream-amqplib 40.82% <ø> (-0.09%) ⬇️
plugins-test-and-upstream-apollo 36.51% <ø> (-0.08%) ⬇️
plugins-test-and-upstream-avsc 35.41% <ø> (-0.09%) ⬇️
plugins-test-and-upstream-bunyan 31.13% <ø> (-0.09%) ⬇️
plugins-test-and-upstream-connect 37.84% <ø> (-0.09%) ⬇️
plugins-test-and-upstream-graphql 37.18% <ø> (-0.09%) ⬇️
plugins-test-and-upstream-koa 37.45% <ø> (-0.09%) ⬇️
plugins-test-and-upstream-protobufjs 35.63% <ø> (-0.09%) ⬇️
plugins-test-and-upstream-rhea 41.06% <ø> (+0.04%) ⬆️
plugins-undici 36.51% <ø> (-0.09%) ⬇️
plugins-url 25.70% <ø> (ø)
plugins-valkey 35.24% <ø> (-0.09%) ⬇️
plugins-vm 25.70% <ø> (ø)
plugins-winston 31.58% <ø> (-0.09%) ⬇️
plugins-ws 38.97% <ø> (-0.09%) ⬇️
profiling-macos 37.82% <ø> (-0.09%) ⬇️
profiling-ubuntu 37.99% <ø> (-0.54%) ⬇️
profiling-windows 39.35% <ø> (-0.09%) ⬇️
serverless-azure-functions-client 25.25% <ø> (ø)
serverless-azure-functions-eventhubs 25.25% <ø> (ø)
serverless-azure-functions-servicebus 25.25% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@crysmags
crysmags removed the request for review from a team April 24, 2026 16:45
@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Apr 24, 2026

Copy link
Copy Markdown

Tests

Fix all issues with BitsAI or with Cursor

⚠️ Warnings

🧪 1 Test failed

TaintTracking lodash should propagate strings with lodash "before all" hook in "should propagate strings with lodash" from should propagate strings with lodash   View in Datadog   (Fix with Cursor)
Timeout of 5000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/runner/work/dd-trace-js/dd-trace-js/packages/dd-trace/test/appsec/iast/taint-tracking/taint-tracking.lodash.plugin.spec.js)

Error: Timeout of 5000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/runner/work/dd-trace-js/dd-trace-js/packages/dd-trace/test/appsec/iast/taint-tracking/taint-tracking.lodash.plugin.spec.js)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

ℹ️ Info

No other issues found (see more)

❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 17.50%
Overall Coverage: 59.96% (+0.58%)

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: cfd4e5e | Docs | Datadog PR Page | Give us feedback!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4096f85158

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}

if (Array.isArray(data)) {
return data.map(item => formatIO(item))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep non-message objects from collapsing to empty content

The new shared formatIO routes every non-plain object to getContentFromMessage, which creates { content: message.content || '' }. In LangGraph workflows, state often contains class instances that are not chat messages (for example Document-like or user-defined objects without a content property), and these now get reduced to {"content":""} instead of preserving their serialized data as before. This is a regression in workflow I/O tagging because it drops meaningful state content for any non-BaseMessage object.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 5503957. formatIO now only routes duck-typed BaseMessage instances (typeof data._getType === 'function' || typeof data.getType === 'function') to getContentFromMessage. Any other class instance falls back to JSON.stringify(data) — matching the pre-#8097 LangGraph behavior so Document-like or user-defined objects without a content property retain their serialized shape instead of collapsing to {content: ''}. Added a regression unit test in packages/dd-trace/test/llmobs/plugins/shared/messages.spec.js ("preserves non-message class instances via JSON.stringify...").

@pr-commenter

pr-commenter Bot commented Apr 24, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-04-27 19:14:13

Comparing candidate commit cfd4e5e in PR branch fix/langgraph-llmobs with baseline commit df1f326 in branch master.

Found 0 performance improvements and 0 performance regressions! Performance is the same for 1344 metrics, 100 unstable metrics.

Addresses Codex review on PR #8097. The new shared formatIO previously
routed every non-plain-object/non-array through getContentFromMessage,
collapsing class instances without a `content` property (e.g. LangChain
Document) to `{ content: '' }` and dropping meaningful state data in
LangGraph workflow I/O.

Only duck-typed BaseMessage instances (have `_getType`/`getType`) now
produce `{ content, role }`; other class instances fall back to
JSON.stringify, matching the pre-#8097 LangGraph behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@sabrenner sabrenner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just some cleanup 🧹 things but lgtm!

Comment thread packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js Outdated
Comment thread packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js Outdated
Comment thread packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js Outdated
Comment thread packages/dd-trace/src/llmobs/plugins/langchain/messages.js
Comment thread packages/dd-trace/test/llmobs/plugins/shared/messages.spec.js Outdated
…hain

Address PR #8097 review from @sabrenner:

- Remove formatIO / getContentFromMessage / getRole wrapper methods on
  LangChainLLMObsHandler; chain.js, embedding.js, vectorstore.js, and
  chat_model.js now import the utility directly.
- Move plugins/shared/messages.js into plugins/langchain/messages.js so
  there's no shared/ dir; LangGraph imports from ../langchain/messages.
- Drop the shared/ unit test file — coverage exists via the LangChain
  and LangGraph integration specs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@crysmags
crysmags merged commit 27c35f7 into master Apr 27, 2026
1090 of 1095 checks passed
@crysmags
crysmags deleted the fix/langgraph-llmobs branch April 27, 2026 20:23
dd-octo-sts Bot pushed a commit that referenced this pull request Apr 28, 2026
* fix(llmobs): render BaseMessage in langgraph workflow I/O (#8096)

Extract the langchain handler's formatIO/getContentFromMessage/getRole into a shared util under and use it for both plugins, so workflow spans and child chat_model spans render messages consistently as { content, role }.
BridgeAR pushed a commit that referenced this pull request Apr 30, 2026
* fix(llmobs): render BaseMessage in langgraph workflow I/O (#8096)

Extract the langchain handler's formatIO/getContentFromMessage/getRole into a shared util under and use it for both plugins, so workflow spans and child chat_model spans render messages consistently as { content, role }.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants