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
17 changes: 12 additions & 5 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -613,21 +613,28 @@ These changes are not caught by TypeScript. If you filter, group, or alert on sp

### Span name changes

Affected SDKs: All SDKs running in the browser.
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/).

In v11, this only affects `pageload` spans. Further ops will follow in future releases.
In v11, this affects `pageload` and `graphql` spans. Further ops will follow in future releases.
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 |
| 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 |
| `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`) |

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.

Child spans of a pageload 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 pageload span without a resolved route is already named `'Pageload'`, so filters matching a URL path no longer apply to it. Match on attributes instead:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,23 @@ describe('GraphQL/Apollo Tests > resolve spans', () => {
origin: 'auto.graphql.diagnostic_channel',
data: expect.objectContaining({
'graphql.operation.type': 'query',
'graphql.processing.type': 'execute',
'graphql.document': '{hello}',
'sentry.origin': 'auto.graphql.diagnostic_channel',
}),
}),
expect.objectContaining({ description: 'graphql.parse' }),
expect.objectContaining({ description: 'graphql.validate' }),
expect.objectContaining({
description: 'graphql.parse',
data: expect.objectContaining({ 'graphql.processing.type': 'parse' }),
}),
expect.objectContaining({
description: 'graphql.validate',
data: expect.objectContaining({ 'graphql.processing.type': 'validate' }),
}),
expect.objectContaining({
description: 'graphql.resolve hello',
data: expect.objectContaining({
'graphql.processing.type': 'resolve',
'graphql.field.name': 'hello',
'graphql.field.path': 'hello',
'graphql.field.type': 'String',
Expand Down Expand Up @@ -61,15 +69,23 @@ describe('GraphQL/Apollo Tests > resolve spans', () => {
origin: 'auto.graphql.diagnostic_channel',
data: expect.objectContaining({
'graphql.operation.type': 'query',
'graphql.processing.type': 'execute',
'graphql.document': '{hello}',
'sentry.origin': 'auto.graphql.diagnostic_channel',
}),
}),
expect.objectContaining({ description: 'graphql.parse' }),
expect.objectContaining({ description: 'graphql.validate' }),
expect.objectContaining({
description: 'graphql.parse',
data: expect.objectContaining({ 'graphql.processing.type': 'parse' }),
}),
expect.objectContaining({
description: 'graphql.validate',
data: expect.objectContaining({ 'graphql.processing.type': 'validate' }),
}),
expect.objectContaining({
description: 'graphql.resolve hello',
data: expect.objectContaining({
'graphql.processing.type': 'resolve',
'graphql.field.name': 'hello',
'graphql.field.path': 'hello',
'graphql.field.type': 'String',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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,
integrations: [Sentry.graphqlIntegration({ ignoreResolveSpans: false })],
transport: loggingTransport,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as Sentry from '@sentry/node';

async function run() {
const { createApolloServer } = await import('../../apollo-server.mjs');
const server = createApolloServer();

await Sentry.startSpan({ name: 'Test Transaction', op: 'transaction' }, async span => {
// Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation
await server.executeOperation({ query: 'query GetHello {hello}' });
await server.executeOperation({
query: 'mutation TestMutation($email: String) { login(email: $email) }',
variables: { email: 'test@email.com' },
});

setTimeout(() => {
span.end();
server.stop();
}, 500);
});
}

run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { SerializedStreamedSpanContainer } from '@sentry/core';
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner';

type StreamedSpan = SerializedStreamedSpanContainer['items'][number];

// Scoped to the `Test Transaction` segment: creating the server parses the schema's typeDefs, which
// emits a parse span under `Test Server Start`.
function graphqlSpans(container: SerializedStreamedSpanContainer): StreamedSpan[] {
return container.items.filter(
item =>
item.attributes['sentry.op']?.value === 'graphql' &&
item.attributes['sentry.segment.name']?.value === 'Test Transaction',
);
}

describe('GraphQL/Apollo Tests > span streaming', () => {
afterAll(() => {
cleanupChildProcesses();
});

createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createTestRunner, test) => {
test('names graphql spans after the operation type, never the operation name or field path', async () => {
await createTestRunner()
.expect({
span: container => {
const spans = graphqlSpans(container);

const executeSpans = spans.filter(span => span.attributes['graphql.operation.type']);
expect(executeSpans.map(span => span.name)).toEqual(['GraphQL query', 'GraphQL mutation']);

// Resolver spans keep the field path as an attribute, but it is unbounded, so it must not
// reach the span name.
const resolveSpans = spans.filter(span => span.attributes['graphql.field.path']);
expect(resolveSpans.map(span => span.attributes['graphql.field.path']?.value)).toEqual(['hello', 'login']);
expect(resolveSpans.map(span => span.name)).toEqual(['GraphQL resolve', 'GraphQL resolve']);

// Parse and validate spans have no operation type, so they are named after the phase.
const phaseSpans = spans.filter(span => !executeSpans.includes(span) && !resolveSpans.includes(span));
expect(phaseSpans.length).toBeGreaterThan(0);
expect(phaseSpans.every(span => ['GraphQL parse', 'GraphQL validate'].includes(span.name))).toBe(true);

expect(spans.some(span => span.name.includes('GetHello') || span.name.includes('TestMutation'))).toBe(
false,
);
},
})
.start()
.completed();
});

test('marks every graphql span with its processing type', async () => {
await createTestRunner()
.expect({
span: container => {
const processingTypes = graphqlSpans(container).map(
span => span.attributes['graphql.processing.type']?.value,
);

expect(processingTypes.sort()).toEqual([
'execute',
'execute',
'parse',
'parse',
'resolve',
'resolve',
'validate',
'validate',
]);
},
})
.start()
.completed();
});

test('records the operations on the segment span without renaming it', async () => {
await createTestRunner()
.expect({
span: container => {
// `Test Server Start` is a segment too, so pick the one the operations ran under.
const segmentSpan = container.items.find(item => item.is_segment && item.name === 'Test Transaction');

expect(segmentSpan).toBeDefined();
// Both operations are recorded here rather than in the name.
expect(segmentSpan?.attributes['sentry.graphql.operation']?.value).toEqual([
'query GetHello',
'mutation TestMutation',
]);
},
})
.start()
.completed();
Comment thread
cursor[bot] marked this conversation as resolved.
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => {
'graphql.document': 'query GetHello {hello}',
'sentry.origin': 'auto.graphql.diagnostic_channel',
'sentry.op': 'graphql',
'graphql.processing.type': 'execute',
},
description: 'query GetHello',
status: 'ok',
Expand Down Expand Up @@ -54,6 +55,7 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => {
}`,
'sentry.origin': 'auto.graphql.diagnostic_channel',
'sentry.op': 'graphql',
'graphql.processing.type': 'execute',
},
description: 'mutation TestMutation',
status: 'ok',
Expand Down Expand Up @@ -83,6 +85,7 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => {
'graphql.document': 'query {hello}',
'sentry.origin': 'auto.graphql.diagnostic_channel',
'sentry.op': 'graphql',
'graphql.processing.type': 'execute',
},
description: 'query',
status: 'ok',
Expand Down Expand Up @@ -113,6 +116,7 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => {
'graphql.document': 'query GetHello {hello}',
'sentry.origin': 'auto.graphql.diagnostic_channel',
'sentry.op': 'graphql',
'graphql.processing.type': 'execute',
},
description: 'query GetHello',
status: 'ok',
Expand All @@ -125,6 +129,7 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => {
'graphql.document': 'query GetWorld {world}',
'sentry.origin': 'auto.graphql.diagnostic_channel',
'sentry.op': 'graphql',
'graphql.processing.type': 'execute',
},
description: 'query GetWorld',
status: 'ok',
Expand Down
18 changes: 11 additions & 7 deletions packages/server-utils/src/integrations/graphql/constants.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
/*
* These mirror the constants in `@sentry/server-utils`'s native graphql subscriber
* (`src/graphql/graphql-dc-subscriber.ts`) so the orchestrion path (graphql v14–16) and the native
* `diagnostics_channel` path (graphql >= 17) emit identical spans — same origin, span names and
* field-attribute keys. `graphql.document`/`graphql.operation.*` and the span `op` come from
* `@sentry/conventions` directly and are imported where used.
*/
// Shared by both graphql paths (orchestrion for v14–16, diagnostics channels for >= 17) so they emit
// identical spans.

export const ORIGIN = 'auto.graphql.diagnostic_channel';

export const SPAN_NAME_PARSE = 'graphql.parse';
export const SPAN_NAME_VALIDATE = 'graphql.validate';
export const SPAN_NAME_EXECUTE = 'graphql.execute';
export const SPAN_NAME_SUBSCRIBE = 'graphql.subscribe';
export const SPAN_NAME_RESOLVE = 'graphql.resolve';

// Inlined until `@sentry/conventions` ships it (https://github.com/getsentry/sentry-conventions/pull/572).
export const GRAPHQL_PROCESSING_TYPE = 'graphql.processing.type';

export const PROCESSING_TYPE_PARSE = 'parse';
export const PROCESSING_TYPE_VALIDATE = 'validate';
export const PROCESSING_TYPE_EXECUTE = 'execute';
export const PROCESSING_TYPE_RESOLVE = 'resolve';

// Field-level resolver-span attributes; not in `@sentry/conventions`.
export const GRAPHQL_FIELD_NAME = 'graphql.field.name';
export const GRAPHQL_FIELD_PATH = 'graphql.field.path';
Expand Down
Loading
Loading