Skip to content

Commit 46fb889

Browse files
committed
handle multiple operations
1 parent 9f7b40d commit 46fb889

File tree

7 files changed

+108
-21
lines changed

7 files changed

+108
-21
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
const Sentry = require('@sentry/node');
2+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
3+
4+
const client = Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
integrations: [Sentry.graphqlIntegration({ useOperationNameForRootSpan: true })],
9+
transport: loggingTransport,
10+
});
11+
12+
const tracer = client.tracer;
13+
14+
// Stop the process from exiting before the transaction is sent
15+
setInterval(() => {}, 1000);
16+
17+
async function run() {
18+
const server = require('../apollo-server')();
19+
20+
await tracer.startActiveSpan(
21+
'test span name',
22+
{
23+
kind: 1,
24+
attributes: { 'http.method': 'GET', 'http.route': '/test-graphql' },
25+
},
26+
async span => {
27+
for (let i = 1; i < 10; i++) {
28+
// Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation
29+
await server.executeOperation({
30+
query: `query GetHello${i} {hello}`,
31+
});
32+
}
33+
34+
setTimeout(() => {
35+
span.end();
36+
server.stop();
37+
}, 500);
38+
},
39+
);
40+
}
41+
42+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
43+
run();

dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/scenario-multiple-operations.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ async function run() {
2626
async span => {
2727
// Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation
2828
await server.executeOperation({
29-
query: 'query GetHello {hello}',
29+
query: 'query GetWorld {world}',
3030
});
3131

3232
await server.executeOperation({
33-
query: 'query GetWorld {world}',
33+
query: 'query GetHello {hello}',
3434
});
3535

3636
setTimeout(() => {

dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => {
105105

106106
test('useOperationNameForRootSpan works with multiple query operations', done => {
107107
const EXPECTED_TRANSACTION = {
108-
transaction: 'GET /test-graphql (query GetHello)',
108+
transaction: 'GET /test-graphql (query GetHello, query GetWorld)',
109109
spans: expect.arrayContaining([
110110
expect.objectContaining({
111111
data: {
@@ -137,4 +137,16 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => {
137137
.expect({ transaction: EXPECTED_TRANSACTION })
138138
.start(done);
139139
});
140+
141+
test('useOperationNameForRootSpan works with more than 5 query operations', done => {
142+
const EXPECTED_TRANSACTION = {
143+
transaction:
144+
'GET /test-graphql (query GetHello1, query GetHello2, query GetHello3, query GetHello4, query GetHello5, +4)',
145+
};
146+
147+
createRunner(__dirname, 'scenario-multiple-operations-many.js')
148+
.expect({ transaction: EXPECTED_START_SERVER_TRANSACTION })
149+
.expect({ transaction: EXPECTED_TRANSACTION })
150+
.start(done);
151+
});
140152
});

packages/node/src/integrations/tracing/graphql.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql';
22
import { defineIntegration, getRootSpan, spanToJSON } from '@sentry/core';
3-
import { parseSpanDescription } from '@sentry/opentelemetry';
3+
import { SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION } from '@sentry/opentelemetry';
44
import type { IntegrationFn } from '@sentry/types';
55
import { generateInstrumentOnce } from '../../otel/instrument';
66

@@ -62,20 +62,24 @@ export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
6262

6363
if (options.useOperationNameForRootSpan && operationType) {
6464
const rootSpan = getRootSpan(span);
65-
const rootSpanDescription = parseSpanDescription(rootSpan);
66-
67-
// We guard to only do this on http.server spans, and only if we have not already set the operation name
68-
if (
69-
parseSpanDescription(rootSpan).op === 'http.server' &&
70-
!spanToJSON(rootSpan).data?.['sentry.skip_span_data_inference']
71-
) {
72-
const rootSpanName = `${rootSpanDescription.description} (${operationType}${
73-
operationName ? ` ${operationName}` : ''
74-
})`;
75-
76-
// Ensure the default http.server span name inferral is skipped
77-
rootSpan.setAttribute('sentry.skip_span_data_inference', true);
78-
rootSpan.updateName(rootSpanName);
65+
66+
// We guard to only do this on http.server spans
67+
68+
const rootSpanAttributes = spanToJSON(rootSpan).data || {};
69+
70+
const existingOperations = rootSpanAttributes[SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION] || [];
71+
72+
const newOperation = operationName ? `${operationType} ${operationName}` : `${operationType}`;
73+
74+
// We keep track of each operation on the root span
75+
// This can either be a string, or an array of strings (if there are multiple operations)
76+
if (Array.isArray(existingOperations)) {
77+
existingOperations.push(newOperation);
78+
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION, existingOperations);
79+
} else if (existingOperations) {
80+
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION, [existingOperations, newOperation]);
81+
} else {
82+
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION, newOperation);
7983
}
8084
}
8185
},

packages/opentelemetry/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { parseSpanDescription } from './utils/parseSpanDescription';
1+
export { SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION } from './semanticAttributes';
22

33
export { getRequestSpanData } from './utils/getRequestSpanData';
44

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
/** If this attribute is true, it means that the parent is a remote span. */
22
export const SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE = 'sentry.parentIsRemote';
3+
4+
// These are not standardized yet, but used by the graphql instrumentation
5+
export const SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION = 'sentry.graphql.operation';

packages/opentelemetry/src/utils/parseSpanDescription.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { SpanAttributes, TransactionSource } from '@sentry/types';
1515
import { getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from '@sentry/utils';
1616

1717
import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core';
18+
import { SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION } from '../semanticAttributes';
1819
import type { AbstractSpan } from '../types';
1920
import { getSpanKind } from './getSpanKind';
2021
import { spanHasAttributes, spanHasName } from './spanTypes';
@@ -136,8 +137,16 @@ export function descriptionForHttpMethod(
136137
return { op: opParts.join('.'), description: name, source: 'custom' };
137138
}
138139

139-
// Ex. description="GET /api/users".
140-
const description = `${httpMethod} ${urlPath}`;
140+
const graphqlOperations = attributes[SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION];
141+
142+
// Ex. GET /api/users
143+
const baseDescription = `${httpMethod} ${urlPath}`;
144+
145+
// When the http span has a graphql operation, append it to the description
146+
// We add these in the graphqlIntegration
147+
const description = graphqlOperations
148+
? `${baseDescription} (${getGraphqlOperationNames(graphqlOperations)})`
149+
: baseDescription;
141150

142151
// If `httpPath` is a root path, then we can categorize the transaction source as route.
143152
const source: TransactionSource = hasRoute || urlPath === '/' ? 'route' : 'url';
@@ -162,6 +171,22 @@ export function descriptionForHttpMethod(
162171
};
163172
}
164173

174+
function getGraphqlOperationNames(attr: AttributeValue): string {
175+
if (Array.isArray(attr)) {
176+
const sorted = attr.slice().sort();
177+
178+
// Up to 5 items, we just add all of them
179+
if (sorted.length < 5) {
180+
return sorted.join(', ');
181+
} else {
182+
// Else, we add the first 5 and the diff of other operations
183+
return `${sorted.slice(0, 5).join(', ')}, +${sorted.length - 5}`;
184+
}
185+
}
186+
187+
return `${attr}`;
188+
}
189+
165190
/** Exported for tests only */
166191
export function getSanitizedUrl(
167192
attributes: Attributes,

0 commit comments

Comments
 (0)