diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs new file mode 100644 index 000000000000..91b4e4b1bae5 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + sendDefaultPii: true, + transport: loggingTransport, + integrations: [ + Sentry.langGraphIntegration({ + recordInputs: true, + recordOutputs: true, + enableTruncation: false, + }), + ], +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/scenario-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/scenario-no-truncation.mjs new file mode 100644 index 000000000000..982e7a69de53 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/scenario-no-truncation.mjs @@ -0,0 +1,46 @@ +import { END, MessagesAnnotation, START, StateGraph } from '@langchain/langgraph'; +import * as Sentry from '@sentry/node'; + +async function run() { + await Sentry.startSpan({ op: 'function', name: 'langgraph-test' }, async () => { + const mockLlm = () => { + return { + messages: [ + { + role: 'assistant', + content: 'Mock LLM response', + response_metadata: { + model_name: 'mock-model', + finish_reason: 'stop', + tokenUsage: { + promptTokens: 20, + completionTokens: 10, + totalTokens: 30, + }, + }, + }, + ], + }; + }; + + const graph = new StateGraph(MessagesAnnotation) + .addNode('agent', mockLlm) + .addEdge(START, 'agent') + .addEdge('agent', END) + .compile({ name: 'weather_assistant' }); + + // Multiple messages with long content (would normally be truncated and popped to last message only) + const longContent = 'A'.repeat(50_000); + await graph.invoke({ + messages: [ + { role: 'user', content: longContent }, + { role: 'assistant', content: 'Some reply' }, + { role: 'user', content: 'Follow-up question' }, + ], + }); + }); + + await Sentry.flush(2000); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index 0b03e59bbfbf..329cb914851a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -4,6 +4,7 @@ import { GEN_AI_AGENT_NAME_ATTRIBUTE, GEN_AI_CONVERSATION_ID_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, + GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_PIPELINE_NAME_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, @@ -364,4 +365,37 @@ describe('LangGraph integration', () => { await createRunner().ignore('event').expect({ transaction: EXPECTED_TRANSACTION_RESUME }).start().completed(); }); }); + + const longContent = 'A'.repeat(50_000); + + const EXPECTED_TRANSACTION_NO_TRUNCATION = { + transaction: 'langgraph-test', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([ + { role: 'user', content: longContent }, + { role: 'assistant', content: 'Some reply' }, + { role: 'user', content: 'Follow-up question' }, + ]), + [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3, + }), + }), + ]), + }; + + createEsmAndCjsTests( + __dirname, + 'scenario-no-truncation.mjs', + 'instrument-no-truncation.mjs', + (createRunner, test) => { + test('does not truncate input messages when enableTruncation is false', async () => { + await createRunner() + .ignore('event') + .expect({ transaction: EXPECTED_TRANSACTION_NO_TRUNCATION }) + .start() + .completed(); + }); + }, + ); }); diff --git a/packages/core/src/tracing/langgraph/index.ts b/packages/core/src/tracing/langgraph/index.ts index c010520d10cc..5230b43bb54d 100644 --- a/packages/core/src/tracing/langgraph/index.ts +++ b/packages/core/src/tracing/langgraph/index.ts @@ -12,8 +12,12 @@ import { GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; -import { truncateGenAiMessages } from '../ai/messageTruncation'; -import { extractSystemInstructions, resolveAIRecordingOptions } from '../ai/utils'; +import { + extractSystemInstructions, + getJsonString, + getTruncatedJsonString, + resolveAIRecordingOptions, +} from '../ai/utils'; import type { LangChainMessage } from '../langchain/types'; import { normalizeLangChainMessages } from '../langchain/utils'; import { startSpan } from '../trace'; @@ -146,10 +150,12 @@ function instrumentCompiledGraphInvoke( span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, systemInstructions); } - const truncatedMessages = truncateGenAiMessages(filteredMessages as unknown[]); + const enableTruncation = options.enableTruncation ?? true; const filteredLength = Array.isArray(filteredMessages) ? filteredMessages.length : 0; span.setAttributes({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify(truncatedMessages), + [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: enableTruncation + ? getTruncatedJsonString(filteredMessages) + : getJsonString(filteredMessages), [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: filteredLength, }); } diff --git a/packages/core/src/tracing/langgraph/types.ts b/packages/core/src/tracing/langgraph/types.ts index b16f9718c69e..021099f369b1 100644 --- a/packages/core/src/tracing/langgraph/types.ts +++ b/packages/core/src/tracing/langgraph/types.ts @@ -7,6 +7,11 @@ export interface LangGraphOptions { * Enable or disable output recording. */ recordOutputs?: boolean; + /** + * Enable or disable truncation of recorded input messages. + * Defaults to `true`. + */ + enableTruncation?: boolean; } /**