Skip to content
Merged
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
47 changes: 0 additions & 47 deletions MIGRATION.md

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should be in the end state file

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ah yes, good catch.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I also moved the already landed pageload/graphql stuff.

Original file line number Diff line number Diff line change
Expand Up @@ -613,53 +613,6 @@ These changes are not caught by TypeScript. If you filter, group, or alert on sp
| `browser.TLS/SSL` | `browser.tls_ssl` |
| `browser.DNS` | `browser.dns` |

### Span name changes

Affected SDKs: All SDKs.

With [span streaming](#span-streaming-is-now-the-default) enabled(the default), span names are now **low cardinality**, following the [Sentry span name conventions](https://getsentry.github.io/sentry-conventions/names/).

If you [opt out of span streaming](#opting-out-of-span-streaming), span names remain unchanged.

The following span names were adjusted:

| Span op | Before | After |
| ------------ | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `pageload` | The parameterized route, or the raw URL path if the SDK couldn't resolve one (`/users/123`) | The parameterized route, or `Pageload` if the SDK has none |
| `router` | Framework-specific, sometimes containing the raw URL (`/users/123`, `SvelteKit Route Change`) | The span's `http.route`, or `Router` if the SDK has none |
| `graphql` | The graphql phase and, for operations, the operation name (`query GetUser`, `graphql.parse`, `graphql.resolve user.0.name`) | The operation type, or the processing type where there is none (`GraphQL query`, `GraphQL parse`, `GraphQL resolve`) |
| `resource.*` | The resource URL, relative to the page origin for same-origin resources (`/assets/app.js`) | The resource domain (`cdn.example.com`), or `Resource` if the SDK has none |

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

Some consequences to be aware of:

The graphql operation name and the resolver field path are supplied by the client, so they are no longer part of a span name. They remain available on the `graphql.operation.name` and `graphql.field.path` attributes.

Because a low-cardinality name cannot say which part of request processing a span covers, every graphql span now carries a `graphql.processing.type` attribute (`parse`, `validate`, `execute` or `resolve`). Use it to tell parse, validate and resolve spans apart. The attribute is set in both trace lifecycles.

For the same reason, `useOperationNameForRootSpan` no longer renames the enclosing root span (`GET /graphql` stays `GET /graphql`, instead of becoming `GET /graphql (query GetUser)`). The operations are still recorded on that span's `sentry.graphql.operation` attribute, as long as the option stays enabled (the default). Disabling it skips both, as before.

Only the Express, Koa and Hapi integrations resolve a route template for `router` spans. Angular, Ember and SvelteKit have none when the span starts, so their router spans are named `Router`.

Child spans of a service or root span carry its name in their `sentry.segment.name` attribute, so that changes with it. If you group or filter spans by segment name in dashboards or alerts, update those references.

`ignoreSpans` is evaluated when a span **starts**, at which point a span might not yet have its final name. For example, an unresolved pageload span name is named `'Pageload'` and might receive its final, resolved route name later.
`ignoreSpans` filters matching a URL path no longer apply to them.
Another example where filters might need adjustments are `resource.*` spans where their name now only includes the domain the resource was taken from.

Match on attributes instead:

```js
Sentry.init({
// Before
ignoreSpans: ['/health'],

// After
ignoreSpans: [{ name: 'Pageload', attributes: { 'sentry.op': 'pageload', 'url.path': '/health' } }],
});
```

### LangGraph no longer emits `create_agent` spans

Affected SDKs: All server-side SDKs.
Expand Down
48 changes: 48 additions & 0 deletions docs/migration/v11-end-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,54 @@ These changes are not caught by TypeScript. If you filter, group, or alert on sp
| `browser.TLS/SSL` | `browser.tls_ssl` |
| `browser.DNS` | `browser.dns` |

### Span name changes

Affected SDKs: All SDKs.

With [span streaming](#span-streaming-is-now-the-default) enabled(the default), span names are now **low cardinality**, following the [Sentry span name conventions](https://getsentry.github.io/sentry-conventions/names/).

If you [opt out of span streaming](#opting-out-of-span-streaming), span names remain unchanged.

The following span names were adjusted:

| Span op | Before | After |
| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `pageload` | The parameterized route, or the raw URL path if the SDK couldn't resolve one (`/users/123`) | The parameterized route, or `Pageload` if the SDK has none |
| `router` | Framework-specific, sometimes containing the raw URL (`/users/123`, `SvelteKit Route Change`) | The span's `http.route`, or `Router` if the SDK has none |
| `graphql` | The graphql phase and, for operations, the operation name (`query GetUser`, `graphql.parse`, `graphql.resolve user.0.name`) | The operation type, or the processing type where there is none (`GraphQL query`, `GraphQL parse`, `GraphQL resolve`) |
| `resource.*` | The resource URL, relative to the page origin for same-origin resources (`/assets/app.js`) | The resource domain (`cdn.example.com`), or `Resource` if the SDK has none |
| `mcp.notification.client_to_server`, `mcp.notification.server_to_client` | The notification method name (`notifications/tools/list_changed`) | The notification method name, or `MCP notification` if the message carries none |

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

Some consequences to be aware of:

The graphql operation name and the resolver field path are supplied by the client, so they are no longer part of a span name. They remain available on the `graphql.operation.name` and `graphql.field.path` attributes.

Because a low-cardinality name cannot say which part of request processing a span covers, every graphql span now carries a `graphql.processing.type` attribute (`parse`, `validate`, `execute` or `resolve`). Use it to tell parse, validate and resolve spans apart. The attribute is set in both trace lifecycles.

For the same reason, `useOperationNameForRootSpan` no longer renames the enclosing root span (`GET /graphql` stays `GET /graphql`, instead of becoming `GET /graphql (query GetUser)`). The operations are still recorded on that span's `sentry.graphql.operation` attribute, as long as the option stays enabled (the default). Disabling it skips both, as before.

Only the Express, Koa and Hapi integrations resolve a route template for `router` spans. Angular, Ember and SvelteKit have none when the span starts, so their router spans are named `Router`.

Child spans of a service or root span carry its name in their `sentry.segment.name` attribute, so that changes with it. If you group or filter spans by segment name in dashboards or alerts, update those references.

`ignoreSpans` is evaluated when a span **starts**, at which point a span might not yet have its final name. For example, an unresolved pageload span name is named `'Pageload'` and might receive its final, resolved route name later.
`ignoreSpans` filters matching a URL path no longer apply to them.
Another example where filters might need adjustments are `resource.*` spans where their name now only includes the domain the resource was taken from.

Match on attributes instead:

```js
Sentry.init({
// Before
ignoreSpans: ['/health'],

// After
ignoreSpans: [{ name: 'Pageload', attributes: { 'sentry.op': 'pageload', 'url.path': '/health' } }],
});
```

### AI integrations no longer trace non-inference operations

Affected SDKs: All server-side SDKs.
Expand Down
9 changes: 6 additions & 3 deletions packages/core/src/integrations/mcp-server/spans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import { getClient } from '../../currentScopes';
import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes';
import { hasSpanStreamingEnabled } from '../../tracing/spans/hasSpanStreamingEnabled';
import { MCP_NOTIFICATION_SPAN_NAME_FALLBACK } from '../../tracing/spans/spanNames';
import { startSpan } from '../../tracing/trace';
import { buildTransportAttributes, buildTypeSpecificAttributes } from './attributeExtraction';
import {
Expand Down Expand Up @@ -83,15 +85,17 @@ function createMcpSpan(config: McpSpanConfig): unknown {
const { type, message, transport, extra, callback, options } = config;
const { method } = message;
const params = message.params;
const client = getClient();

// Determine span name based on type and OTEL conventions
let spanName: string;
if (type === 'request') {
const targetInfo = extractTargetInfo(method, params || {});
spanName = createSpanName(method, targetInfo.target);
} else {
// For notifications, use method name directly per OpenTelemetry conventions
spanName = method;
// For notifications, use method name directly per OpenTelemetry conventions.
// With span streaming, span names have to be low cardinality, so a message without a method name gets a static name.
spanName = method || (!!client && hasSpanStreamingEnabled(client) ? MCP_NOTIFICATION_SPAN_NAME_FALLBACK : method);
Comment thread
andreiborza marked this conversation as resolved.
}

const rawAttributes: Record<string, string | number> = {
Expand All @@ -101,7 +105,6 @@ function createMcpSpan(config: McpSpanConfig): unknown {
...buildSentryAttributes(type),
};

const client = getClient();
const userInfo = Boolean(client?.getDataCollectionOptions().userInfo);
const attributes = filterMcpPiiFromSpanData(rawAttributes, userInfo) as Record<string, string | number>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,38 @@ describe('MCP Server Semantic Conventions', () => {
);
});

it('should keep the method name as the notification span name when span streaming is enabled', async () => {
getClientSpy.mockReturnValue(createMockClient(true, undefined, 'stream'));
await wrappedMcpServer.connect(mockTransport);

mockTransport.onmessage?.({ jsonrpc: '2.0', method: 'notifications/tools/list_changed', params: {} }, {});

expect(startSpanSpy).toHaveBeenCalledWith(
expect.objectContaining({ name: 'notifications/tools/list_changed' }),
expect.any(Function),
);
});

it('should fall back to a low cardinality notification span name when the method name is missing', async () => {
getClientSpy.mockReturnValue(createMockClient(true, undefined, 'stream'));
await wrappedMcpServer.connect(mockTransport);

mockTransport.onmessage?.({ jsonrpc: '2.0', method: null, params: {} }, {});

expect(startSpanSpy).toHaveBeenCalledWith(
expect.objectContaining({ name: 'MCP notification' }),
expect.any(Function),
);
});

it('should not fall back to a low cardinality notification span name in static trace lifecycle mode', async () => {
await wrappedMcpServer.connect(mockTransport);

mockTransport.onmessage?.({ jsonrpc: '2.0', method: null, params: {} }, {});

expect(startSpanSpy).toHaveBeenCalledWith(expect.objectContaining({ name: null }), expect.any(Function));
});
Comment thread
andreiborza marked this conversation as resolved.

it('should instrument tool call results and complete span with enriched attributes', async () => {
await wrappedMcpServer.connect(mockTransport);

Expand Down
9 changes: 7 additions & 2 deletions packages/core/test/lib/integrations/mcp-server/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ import { vi } from 'vitest';
* Creates a mock Sentry client with getDataCollectionOptions for use in MCP server tests.
* @param userInfo - Whether user identity data (IP, port) is collected. Default: true
* @param genAI - Whether AI inputs/outputs are recorded. Defaults to match userInfo.
* @param traceLifecycle - Whether spans are streamed or sent as transactions. Default: 'static'
*/
export function createMockClient(userInfo = true, genAI?: { inputs: boolean; outputs: boolean }): Client {
export function createMockClient(
userInfo = true,
genAI?: { inputs: boolean; outputs: boolean },
traceLifecycle: 'static' | 'stream' = 'static',
): Client {
const genAIOptions = genAI ?? { inputs: userInfo, outputs: userInfo };
return {
getOptions: () => ({}),
getOptions: () => ({ traceLifecycle }),
getDataCollectionOptions: () => ({
userInfo,
cookies: true,
Expand Down
Loading