Skip to content

Commit 691d17b

Browse files
committed
feat(core): Add enableTruncation option to LangChain integration
This PR adds an `enableTruncation` option to the LangChain integration that allows users to disable input message truncation. It defaults to `true` to preserve existing behavior. Also fixes missing truncation for LLM string prompts in extractLLMRequestAttributes and refactors to use the shared getTruncatedJsonString/getJsonString utilities. Closes: #20138
1 parent 86dc30a commit 691d17b

File tree

8 files changed

+131
-6
lines changed

8 files changed

+131
-6
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
sendDefaultPii: true,
9+
transport: loggingTransport,
10+
integrations: [
11+
Sentry.langchainIntegration({
12+
enableTruncation: false,
13+
}),
14+
],
15+
beforeSendTransaction: event => {
16+
// Filter out mock express server transactions
17+
if (event.transaction.includes('/v1/messages') || event.transaction.includes('/v1/embeddings')) {
18+
return null;
19+
}
20+
return event;
21+
},
22+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { ChatAnthropic } from '@langchain/anthropic';
2+
import * as Sentry from '@sentry/node';
3+
import express from 'express';
4+
5+
function startMockAnthropicServer() {
6+
const app = express();
7+
app.use(express.json({ limit: '10mb' }));
8+
9+
app.post('/v1/messages', (req, res) => {
10+
res.json({
11+
id: 'msg_no_truncation_test',
12+
type: 'message',
13+
role: 'assistant',
14+
content: [{ type: 'text', text: 'Response' }],
15+
model: req.body.model,
16+
stop_reason: 'end_turn',
17+
stop_sequence: null,
18+
usage: { input_tokens: 10, output_tokens: 5 },
19+
});
20+
});
21+
22+
return new Promise(resolve => {
23+
const server = app.listen(0, () => {
24+
resolve(server);
25+
});
26+
});
27+
}
28+
29+
async function run() {
30+
const server = await startMockAnthropicServer();
31+
const baseUrl = `http://localhost:${server.address().port}`;
32+
33+
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
34+
const model = new ChatAnthropic({
35+
model: 'claude-3-5-sonnet-20241022',
36+
apiKey: 'mock-api-key',
37+
clientOptions: {
38+
baseURL: baseUrl,
39+
},
40+
});
41+
42+
// Long content that would normally be truncated
43+
const longContent = 'A'.repeat(50_000);
44+
await model.invoke([{ role: 'user', content: longContent }]);
45+
});
46+
47+
await Sentry.flush(2000);
48+
49+
server.close();
50+
}
51+
52+
run();

dev-packages/node-integration-tests/suites/tracing/langchain/test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,4 +549,32 @@ describe('LangChain integration', () => {
549549
.completed();
550550
});
551551
});
552+
553+
const longContent = 'A'.repeat(50_000);
554+
555+
const EXPECTED_TRANSACTION_NO_TRUNCATION = {
556+
transaction: 'main',
557+
spans: expect.arrayContaining([
558+
expect.objectContaining({
559+
data: expect.objectContaining({
560+
[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringContaining(longContent),
561+
}),
562+
}),
563+
]),
564+
};
565+
566+
createEsmAndCjsTests(
567+
__dirname,
568+
'scenario-no-truncation.mjs',
569+
'instrument-no-truncation.mjs',
570+
(createRunner, test) => {
571+
test('does not truncate input messages when enableTruncation is false', async () => {
572+
await createRunner()
573+
.ignore('event')
574+
.expect({ transaction: EXPECTED_TRANSACTION_NO_TRUNCATION })
575+
.start()
576+
.completed();
577+
});
578+
},
579+
);
552580
});

packages/core/.oxlintrc.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
"rules": {
1717
"sdk/no-unsafe-random-apis": "off"
1818
}
19+
},
20+
{
21+
"files": ["src/tracing/langchain/utils.ts"],
22+
"rules": {
23+
"max-lines": "off"
24+
}
1925
}
2026
],
2127
"ignorePatterns": ["rollup.npm.config.mjs"]

packages/core/src/tracing/langchain/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
*/
3535
export function createLangChainCallbackHandler(options: LangChainOptions = {}): LangChainCallbackHandler {
3636
const { recordInputs, recordOutputs } = resolveAIRecordingOptions(options);
37+
const enableTruncation = options.enableTruncation ?? true;
3738

3839
// Internal state - single instance tracks all spans
3940
const spanMap = new Map<string, Span>();
@@ -89,6 +90,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
8990
llm as LangChainSerialized,
9091
prompts,
9192
recordInputs,
93+
enableTruncation,
9294
invocationParams,
9395
metadata,
9496
);
@@ -127,6 +129,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
127129
llm as LangChainSerialized,
128130
messages as LangChainMessage[][],
129131
recordInputs,
132+
enableTruncation,
130133
invocationParams,
131134
metadata,
132135
);

packages/core/src/tracing/langchain/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ export interface LangChainOptions {
1313
* @default false (respects sendDefaultPii option)
1414
*/
1515
recordOutputs?: boolean;
16+
17+
/**
18+
* Enable or disable truncation of recorded input messages.
19+
* Defaults to `true`.
20+
*/
21+
enableTruncation?: boolean;
1622
}
1723

1824
/**

packages/core/src/tracing/langchain/utils.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ import {
2626
GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE,
2727
} from '../ai/gen-ai-attributes';
2828
import { isContentMedia, stripInlineMediaFromSingleMessage } from '../ai/mediaStripping';
29-
import { truncateGenAiMessages } from '../ai/messageTruncation';
30-
import { extractSystemInstructions } from '../ai/utils';
29+
import { extractSystemInstructions, getJsonString, getTruncatedJsonString } from '../ai/utils';
3130
import { LANGCHAIN_ORIGIN, ROLE_MAP } from './constants';
3231
import type { LangChainLLMResult, LangChainMessage, LangChainSerialized } from './types';
3332

@@ -284,6 +283,7 @@ export function extractLLMRequestAttributes(
284283
llm: LangChainSerialized,
285284
prompts: string[],
286285
recordInputs: boolean,
286+
enableTruncation: boolean,
287287
invocationParams?: Record<string, unknown>,
288288
langSmithMetadata?: Record<string, unknown>,
289289
): Record<string, SpanAttributeValue> {
@@ -295,7 +295,11 @@ export function extractLLMRequestAttributes(
295295
if (recordInputs && Array.isArray(prompts) && prompts.length > 0) {
296296
setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, prompts.length);
297297
const messages = prompts.map(p => ({ role: 'user', content: p }));
298-
setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, asString(messages));
298+
setIfDefined(
299+
attrs,
300+
GEN_AI_INPUT_MESSAGES_ATTRIBUTE,
301+
enableTruncation ? getTruncatedJsonString(messages) : getJsonString(messages),
302+
);
299303
}
300304

301305
return attrs;
@@ -314,6 +318,7 @@ export function extractChatModelRequestAttributes(
314318
llm: LangChainSerialized,
315319
langChainMessages: LangChainMessage[][],
316320
recordInputs: boolean,
321+
enableTruncation: boolean,
317322
invocationParams?: Record<string, unknown>,
318323
langSmithMetadata?: Record<string, unknown>,
319324
): Record<string, SpanAttributeValue> {
@@ -334,8 +339,11 @@ export function extractChatModelRequestAttributes(
334339
const filteredLength = Array.isArray(filteredMessages) ? filteredMessages.length : 0;
335340
setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, filteredLength);
336341

337-
const truncated = truncateGenAiMessages(filteredMessages as unknown[]);
338-
setIfDefined(attrs, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, asString(truncated));
342+
setIfDefined(
343+
attrs,
344+
GEN_AI_INPUT_MESSAGES_ATTRIBUTE,
345+
enableTruncation ? getTruncatedJsonString(filteredMessages) : getJsonString(filteredMessages),
346+
);
339347
}
340348

341349
return attrs;

packages/core/test/lib/tracing/langchain-utils.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ describe('extractChatModelRequestAttributes with multimodal content', () => {
237237
],
238238
];
239239

240-
const attrs = extractChatModelRequestAttributes(serialized, messages, true);
240+
const attrs = extractChatModelRequestAttributes(serialized, messages, true, true);
241241
const inputMessages = attrs[GEN_AI_INPUT_MESSAGES_ATTRIBUTE] as string | undefined;
242242

243243
expect(inputMessages).toBeDefined();

0 commit comments

Comments
 (0)