Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

Work in this release was contributed by @psh4607, @thijsw, @trinitiwowka, @nehaprasad-dev, @JealousGx, @Jxxunnn, @eddie333016, @davidmurdoch, @yashschandra, @atharv-sys32, @AG0708, @birkskyum, @mkly, @mcbbugu, and @suhailopensource. Thank you for your contributions!

- feat(langchain)!: Emit `gen_ai.pipeline.name` instead of `langchain.chain.name` on LangChain chain spans. The attribute is omitted when the chain is unnamed.
- feat(deno)!: Rename several default integrations to match the other SDKs ([#22404](https://github.com/getsentry/sentry-javascript/pull/22404)). The `deno*Integration` exports are kept as deprecated aliases. If you were relying on the names (for example, to disable them), then note that these have changed:
- `DenoAmqplib` => `Amqplib`
- `DenoKoa` => `Koa`
Expand Down
3 changes: 2 additions & 1 deletion MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,7 @@ Attribute availability remains runtime-dependent. For example, browser and Worke
- The Vercel AI token attributes `gen_ai.usage.input_tokens.cached`, `gen_ai.usage.input_tokens.cache_write`, and `gen_ai.usage.output_tokens.reasoning` were renamed to `gen_ai.usage.cache_read.input_tokens`, `gen_ai.usage.cache_creation.input_tokens`, and `gen_ai.usage.reasoning.output_tokens`.
- The deprecated `gen_ai.tool.type` span attribute is no longer set on tool spans.
- The `ai.pipeline.name` and `ai.streaming` span attributes on Vercel AI spans were renamed to `gen_ai.pipeline.name` and `gen_ai.response.streaming`.
- The `langchain.chain.name` span attribute on LangChain chain spans was renamed to `gen_ai.pipeline.name`. The attribute is omitted when the chain is unnamed, rather than writing `unknown_chain`.
- The `gen_ai.prompt` span attribute is no longer set by the Anthropic integration. The legacy Completions API's `prompt` is now reported as a user message on `gen_ai.input.messages`, like every other request shape.

#### Other attributes
Expand Down Expand Up @@ -856,7 +857,7 @@ The following span names were adjusted:

Resolved low-cardinality values are kept in both lifecycles: a known model stays in the name (`chat gpt-4`).

LangChain agent spans now lead with the operation, like LangGraph and Vercel AI ones: `chain format_prompt` becomes `invoke_agent format_prompt`, and a chain the SDK cannot name becomes `invoke_agent` rather than `chain unknown_chain` — update any `ignoreSpans` filters matching the old names. The chain name remains available on `langchain.chain.name`. LangGraph agent names and Vercel AI `functionId`s are unchanged.
LangChain agent spans now lead with the operation, like LangGraph and Vercel AI ones: `chain format_prompt` becomes `invoke_agent format_prompt`, and a chain the SDK cannot name becomes `invoke_agent` rather than `chain unknown_chain` — update any `ignoreSpans` filters matching the old names. The chain name remains available on `gen_ai.pipeline.name`. LangGraph agent names and Vercel AI `functionId`s are unchanged.

Resource spans now also carry a `url.domain` attribute holding that domain. The full URL remains available on `url.full`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
GEN_AI_EMBEDDINGS_INPUT,
GEN_AI_INPUT_MESSAGES,
GEN_AI_OPERATION_NAME,
GEN_AI_PIPELINE_NAME,
GEN_AI_PROVIDER_NAME,
GEN_AI_REQUEST_MAX_TOKENS,
GEN_AI_REQUEST_MODEL,
Expand Down Expand Up @@ -270,7 +271,8 @@ describe('LangChain integration', () => {
expect(formatPromptSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent');
expect(formatPromptSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain');
expect(formatPromptSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('invoke_agent');
expect(formatPromptSpan!.attributes['langchain.chain.name'].value).toBe('format_prompt');
expect(formatPromptSpan!.attributes[GEN_AI_PIPELINE_NAME].value).toBe('format_prompt');
expect(formatPromptSpan!.attributes['langchain.chain.name']).toBeUndefined();

const chatSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022');
expect(chatSpan).toBeDefined();
Expand All @@ -281,11 +283,14 @@ describe('LangChain integration', () => {
expect(parseOutputSpan).toBeDefined();
expect(parseOutputSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent');
expect(parseOutputSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain');
expect(parseOutputSpan!.attributes['langchain.chain.name'].value).toBe('parse_output');
expect(parseOutputSpan!.attributes[GEN_AI_PIPELINE_NAME].value).toBe('parse_output');
expect(parseOutputSpan!.attributes['langchain.chain.name']).toBeUndefined();

const unknownChainSpan = container.items.find(span => span.name === 'chain unknown_chain');
expect(unknownChainSpan).toBeDefined();
expect(unknownChainSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent');
expect(unknownChainSpan!.attributes[GEN_AI_PIPELINE_NAME]).toBeUndefined();
expect(unknownChainSpan!.attributes['langchain.chain.name']).toBeUndefined();
},
})
.start()
Expand Down Expand Up @@ -429,11 +434,16 @@ describe('LangChain integration', () => {
for (const span of chainSpans) {
expect(span.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('invoke_agent');
}
expect(chainSpans.map(span => span.attributes['langchain.chain.name']?.value).sort()).toEqual([
'format_prompt',
'parse_output',
'unknown_chain',
]);
expect(
chainSpans
.map(span => span.attributes[GEN_AI_PIPELINE_NAME]?.value)
.filter(Boolean)
.sort(),
).toEqual(['format_prompt', 'parse_output']);
const unnamedChainSpan = chainSpans.find(span => span.name === 'invoke_agent');
expect(unnamedChainSpan).toBeDefined();
expect(unnamedChainSpan!.attributes[GEN_AI_PIPELINE_NAME]).toBeUndefined();
expect(unnamedChainSpan!.attributes['langchain.chain.name']).toBeUndefined();
},
})
.start()
Expand Down
16 changes: 10 additions & 6 deletions packages/server-utils/src/ai/langchain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import type { Span, SpanAttributeValue } from '@sentry/core';
import {
GEN_AI_OPERATION_NAME,
GEN_AI_PIPELINE_NAME,
GEN_AI_REQUEST_MODEL,
GEN_AI_TOOL_CALL_ARGUMENTS,
GEN_AI_TOOL_CALL_RESULT,
Expand Down Expand Up @@ -225,13 +226,16 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
return;
}

const chainName = runName || chain.name || 'unknown_chain';
const chainName = runName || chain.name;
const attributes: Record<string, SpanAttributeValue> = {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain',
[GEN_AI_OPERATION_NAME]: 'invoke_agent',
'langchain.chain.name': chainName,
};

if (chainName) {
attributes[GEN_AI_PIPELINE_NAME] = chainName;
}

if (recordInputs) {
attributes['langchain.chain.inputs'] = JSON.stringify(inputs);
}
Expand All @@ -243,10 +247,10 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
// With span streaming, the name leads with the operation per the agent templates. The
// chain name is bounded, so it stays; the `'unknown_chain'` sentinel is dropped instead.
name: !(client && hasSpanStreamingEnabled(client))
? `chain ${chainName}`
: chainName === 'unknown_chain'
? 'invoke_agent'
: `invoke_agent ${chainName}`,
? `chain ${chainName || 'unknown_chain'}`
: chainName
? `invoke_agent ${chainName}`
: 'invoke_agent',
op: 'gen_ai.invoke_agent',
attributes: {
...attributes,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { GEN_AI_OPERATION_NAME } from '@sentry/conventions/attributes';
import { GEN_AI_OPERATION_NAME, GEN_AI_PIPELINE_NAME } from '@sentry/conventions/attributes';
import { getMainCarrier, setCurrentClient, spanToStaticSpanJSON } from '@sentry/core';
import type { Span } from '@sentry/core';
import { createLangChainCallbackHandler } from '../../../../src/ai/langchain';
Expand Down Expand Up @@ -49,15 +49,30 @@ describe('LangChain invoke_agent span names', () => {
const endedSpans = setupClient('static');
runChain('format_prompt');

expect(spanToStaticSpanJSON(endedSpans[0]!).description).toBe('chain format_prompt');
expect(spanToStaticSpanJSON(endedSpans[0]!).data?.[GEN_AI_OPERATION_NAME]).toBe('invoke_agent');
const span = spanToStaticSpanJSON(endedSpans[0]!);
expect(span.description).toBe('chain format_prompt');
expect(span.data?.[GEN_AI_OPERATION_NAME]).toBe('invoke_agent');
expect(span.data?.[GEN_AI_PIPELINE_NAME]).toBe('format_prompt');
expect(span.data?.['langchain.chain.name']).toBeUndefined();
});

it('sets gen_ai.pipeline.name from chain.name when runName is missing', () => {
const endedSpans = setupClient('static');
runChain(undefined, { name: 'RunnableSequence' });

const span = spanToStaticSpanJSON(endedSpans[0]!);
expect(span.description).toBe('chain RunnableSequence');
expect(span.data?.[GEN_AI_PIPELINE_NAME]).toBe('RunnableSequence');
});

it('keeps `chain unknown_chain` when the chain name is missing in static mode', () => {
const endedSpans = setupClient('static');
runChain();

expect(spanToStaticSpanJSON(endedSpans[0]!).description).toBe('chain unknown_chain');
const span = spanToStaticSpanJSON(endedSpans[0]!);
expect(span.description).toBe('chain unknown_chain');
expect(span.data?.[GEN_AI_PIPELINE_NAME]).toBeUndefined();
expect(span.data?.['langchain.chain.name']).toBeUndefined();
});

it('leads with the operation and keeps the chain name when span streaming is enabled', () => {
Expand All @@ -67,13 +82,17 @@ describe('LangChain invoke_agent span names', () => {
const span = spanToStaticSpanJSON(endedSpans[0]!);
expect(span.description).toBe('invoke_agent format_prompt');
expect(span.data?.[GEN_AI_OPERATION_NAME]).toBe('invoke_agent');
expect(span.data?.['langchain.chain.name']).toBe('format_prompt');
expect(span.data?.[GEN_AI_PIPELINE_NAME]).toBe('format_prompt');
expect(span.data?.['langchain.chain.name']).toBeUndefined();
});

it('drops the `unknown_chain` sentinel when span streaming is enabled', () => {
const endedSpans = setupClient('stream');
runChain();

expect(spanToStaticSpanJSON(endedSpans[0]!).description).toBe('invoke_agent');
const span = spanToStaticSpanJSON(endedSpans[0]!);
expect(span.description).toBe('invoke_agent');
expect(span.data?.[GEN_AI_PIPELINE_NAME]).toBeUndefined();
expect(span.data?.['langchain.chain.name']).toBeUndefined();
});
});
Loading