diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs new file mode 100644 index 000000000000..027299eeacad --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs @@ -0,0 +1,24 @@ +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.langChainIntegration({ + enableTruncation: false, + recordInputs: true, + recordOutputs: true, + }), + ], + beforeSendTransaction: event => { + // Filter out mock express server transactions + if (event.transaction.includes('/v1/messages') || event.transaction.includes('/v1/embeddings')) { + return null; + } + return event; + }, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs new file mode 100644 index 000000000000..bb8f5fc35325 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-no-truncation.mjs @@ -0,0 +1,56 @@ +import { ChatAnthropic } from '@langchain/anthropic'; +import * as Sentry from '@sentry/node'; +import express from 'express'; + +function startMockAnthropicServer() { + const app = express(); + app.use(express.json({ limit: '10mb' })); + + app.post('/v1/messages', (req, res) => { + res.json({ + id: 'msg_no_truncation_test', + type: 'message', + role: 'assistant', + content: [{ type: 'text', text: 'Response' }], + model: req.body.model, + stop_reason: 'end_turn', + stop_sequence: null, + usage: { input_tokens: 10, output_tokens: 5 }, + }); + }); + + return new Promise(resolve => { + const server = app.listen(0, () => { + resolve(server); + }); + }); +} + +async function run() { + const server = await startMockAnthropicServer(); + const baseUrl = `http://localhost:${server.address().port}`; + + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + const model = new ChatAnthropic({ + model: 'claude-3-5-sonnet-20241022', + apiKey: 'mock-api-key', + clientOptions: { + baseURL: baseUrl, + }, + }); + + // Long content that would normally be truncated + const longContent = 'A'.repeat(50_000); + await model.invoke([ + { role: 'user', content: longContent }, + { role: 'assistant', content: 'Some reply' }, + { role: 'user', content: 'Follow-up question' }, + ]); + }); + + await Sentry.flush(2000); + + server.close(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts index 39127c7e3055..434001c92965 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -549,4 +549,37 @@ describe('LangChain integration', () => { .completed(); }); }); + + const longContent = 'A'.repeat(50_000); + + const EXPECTED_TRANSACTION_NO_TRUNCATION = { + transaction: 'main', + 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/.oxlintrc.json b/packages/core/.oxlintrc.json index 05b86efa950b..df7465d97fa2 100644 --- a/packages/core/.oxlintrc.json +++ b/packages/core/.oxlintrc.json @@ -16,6 +16,12 @@ "rules": { "sdk/no-unsafe-random-apis": "off" } + }, + { + "files": ["src/tracing/langchain/utils.ts"], + "rules": { + "max-lines": "off" + } } ], "ignorePatterns": ["rollup.npm.config.mjs"] diff --git a/packages/core/src/tracing/langchain/index.ts b/packages/core/src/tracing/langchain/index.ts index 16257acebbd7..7acc35400c99 100644 --- a/packages/core/src/tracing/langchain/index.ts +++ b/packages/core/src/tracing/langchain/index.ts @@ -34,6 +34,7 @@ import { */ export function createLangChainCallbackHandler(options: LangChainOptions = {}): LangChainCallbackHandler { const { recordInputs, recordOutputs } = resolveAIRecordingOptions(options); + const enableTruncation = options.enableTruncation ?? true; // Internal state - single instance tracks all spans const spanMap = new Map(); @@ -89,6 +90,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): llm as LangChainSerialized, prompts, recordInputs, + enableTruncation, invocationParams, metadata, ); @@ -127,6 +129,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): llm as LangChainSerialized, messages as LangChainMessage[][], recordInputs, + enableTruncation, invocationParams, metadata, ); diff --git a/packages/core/src/tracing/langchain/types.ts b/packages/core/src/tracing/langchain/types.ts index 7379de764817..1c066269aba5 100644 --- a/packages/core/src/tracing/langchain/types.ts +++ b/packages/core/src/tracing/langchain/types.ts @@ -13,6 +13,12 @@ export interface LangChainOptions { * @default false (respects sendDefaultPii option) */ recordOutputs?: boolean; + + /** + * Enable or disable truncation of recorded input messages. + * Defaults to `true`. + */ + enableTruncation?: boolean; } /** diff --git a/packages/core/src/tracing/langchain/utils.ts b/packages/core/src/tracing/langchain/utils.ts index 924739485948..1227889f210d 100644 --- a/packages/core/src/tracing/langchain/utils.ts +++ b/packages/core/src/tracing/langchain/utils.ts @@ -26,8 +26,7 @@ import { GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, } from '../ai/gen-ai-attributes'; import { isContentMedia, stripInlineMediaFromSingleMessage } from '../ai/mediaStripping'; -import { truncateGenAiMessages } from '../ai/messageTruncation'; -import { extractSystemInstructions } from '../ai/utils'; +import { extractSystemInstructions, getJsonString, getTruncatedJsonString } from '../ai/utils'; import { LANGCHAIN_ORIGIN, ROLE_MAP } from './constants'; import type { LangChainLLMResult, LangChainMessage, LangChainSerialized } from './types'; @@ -284,6 +283,7 @@ export function extractLLMRequestAttributes( llm: LangChainSerialized, prompts: string[], recordInputs: boolean, + enableTruncation: boolean, invocationParams?: Record, langSmithMetadata?: Record, ): Record { @@ -295,7 +295,11 @@ export function extractLLMRequestAttributes( if (recordInputs && Array.isArray(prompts) && prompts.length > 0) { setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, prompts.length); const messages = prompts.map(p => ({ role: 'user', content: p })); - setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, asString(messages)); + setIfDefined( + attrs, + GEN_AI_INPUT_MESSAGES_ATTRIBUTE, + enableTruncation ? getTruncatedJsonString(messages) : getJsonString(messages), + ); } return attrs; @@ -314,6 +318,7 @@ export function extractChatModelRequestAttributes( llm: LangChainSerialized, langChainMessages: LangChainMessage[][], recordInputs: boolean, + enableTruncation: boolean, invocationParams?: Record, langSmithMetadata?: Record, ): Record { @@ -334,8 +339,11 @@ export function extractChatModelRequestAttributes( const filteredLength = Array.isArray(filteredMessages) ? filteredMessages.length : 0; setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, filteredLength); - const truncated = truncateGenAiMessages(filteredMessages as unknown[]); - setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, asString(truncated)); + setIfDefined( + attrs, + GEN_AI_INPUT_MESSAGES_ATTRIBUTE, + enableTruncation ? getTruncatedJsonString(filteredMessages) : getJsonString(filteredMessages), + ); } return attrs; diff --git a/packages/core/test/lib/tracing/langchain-utils.test.ts b/packages/core/test/lib/tracing/langchain-utils.test.ts index 98724c8902d4..18807631c404 100644 --- a/packages/core/test/lib/tracing/langchain-utils.test.ts +++ b/packages/core/test/lib/tracing/langchain-utils.test.ts @@ -237,7 +237,7 @@ describe('extractChatModelRequestAttributes with multimodal content', () => { ], ]; - const attrs = extractChatModelRequestAttributes(serialized, messages, true); + const attrs = extractChatModelRequestAttributes(serialized, messages, true, true); const inputMessages = attrs[GEN_AI_INPUT_MESSAGES_ATTRIBUTE] as string | undefined; expect(inputMessages).toBeDefined();