Skip to content

Commit

Permalink
handle multiple operations
Browse files Browse the repository at this point in the history
  • Loading branch information
mydea committed Aug 6, 2024
1 parent 9f7b40d commit 46fb889
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const Sentry = require('@sentry/node');
const { loggingTransport } = require('@sentry-internal/node-integration-tests');

const client = Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
integrations: [Sentry.graphqlIntegration({ useOperationNameForRootSpan: true })],
transport: loggingTransport,
});

const tracer = client.tracer;

// Stop the process from exiting before the transaction is sent
setInterval(() => {}, 1000);

async function run() {
const server = require('../apollo-server')();

await tracer.startActiveSpan(
'test span name',
{
kind: 1,
attributes: { 'http.method': 'GET', 'http.route': '/test-graphql' },
},
async span => {
for (let i = 1; i < 10; i++) {
// Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation
await server.executeOperation({
query: `query GetHello${i} {hello}`,
});
}

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

// eslint-disable-next-line @typescript-eslint/no-floating-promises
run();
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ async function run() {
async span => {
// Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation
await server.executeOperation({
query: 'query GetHello {hello}',
query: 'query GetWorld {world}',
});

await server.executeOperation({
query: 'query GetWorld {world}',
query: 'query GetHello {hello}',
});

setTimeout(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => {

test('useOperationNameForRootSpan works with multiple query operations', done => {
const EXPECTED_TRANSACTION = {
transaction: 'GET /test-graphql (query GetHello)',
transaction: 'GET /test-graphql (query GetHello, query GetWorld)',
spans: expect.arrayContaining([
expect.objectContaining({
data: {
Expand Down Expand Up @@ -137,4 +137,16 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => {
.expect({ transaction: EXPECTED_TRANSACTION })
.start(done);
});

test('useOperationNameForRootSpan works with more than 5 query operations', done => {
const EXPECTED_TRANSACTION = {
transaction:
'GET /test-graphql (query GetHello1, query GetHello2, query GetHello3, query GetHello4, query GetHello5, +4)',
};

createRunner(__dirname, 'scenario-multiple-operations-many.js')
.expect({ transaction: EXPECTED_START_SERVER_TRANSACTION })
.expect({ transaction: EXPECTED_TRANSACTION })
.start(done);
});
});
34 changes: 19 additions & 15 deletions packages/node/src/integrations/tracing/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql';
import { defineIntegration, getRootSpan, spanToJSON } from '@sentry/core';
import { parseSpanDescription } from '@sentry/opentelemetry';
import { SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION } from '@sentry/opentelemetry';
import type { IntegrationFn } from '@sentry/types';
import { generateInstrumentOnce } from '../../otel/instrument';

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

if (options.useOperationNameForRootSpan && operationType) {
const rootSpan = getRootSpan(span);
const rootSpanDescription = parseSpanDescription(rootSpan);

// We guard to only do this on http.server spans, and only if we have not already set the operation name
if (
parseSpanDescription(rootSpan).op === 'http.server' &&
!spanToJSON(rootSpan).data?.['sentry.skip_span_data_inference']
) {
const rootSpanName = `${rootSpanDescription.description} (${operationType}${
operationName ? ` ${operationName}` : ''
})`;

// Ensure the default http.server span name inferral is skipped
rootSpan.setAttribute('sentry.skip_span_data_inference', true);
rootSpan.updateName(rootSpanName);

// We guard to only do this on http.server spans

const rootSpanAttributes = spanToJSON(rootSpan).data || {};

const existingOperations = rootSpanAttributes[SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION] || [];

const newOperation = operationName ? `${operationType} ${operationName}` : `${operationType}`;

// We keep track of each operation on the root span
// This can either be a string, or an array of strings (if there are multiple operations)
if (Array.isArray(existingOperations)) {
existingOperations.push(newOperation);
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION, existingOperations);
} else if (existingOperations) {
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION, [existingOperations, newOperation]);
} else {
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION, newOperation);
}
}
},
Expand Down
2 changes: 1 addition & 1 deletion packages/opentelemetry/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { parseSpanDescription } from './utils/parseSpanDescription';
export { SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION } from './semanticAttributes';

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

Expand Down
3 changes: 3 additions & 0 deletions packages/opentelemetry/src/semanticAttributes.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
/** If this attribute is true, it means that the parent is a remote span. */
export const SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE = 'sentry.parentIsRemote';

// These are not standardized yet, but used by the graphql instrumentation
export const SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION = 'sentry.graphql.operation';
29 changes: 27 additions & 2 deletions packages/opentelemetry/src/utils/parseSpanDescription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { SpanAttributes, TransactionSource } from '@sentry/types';
import { getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from '@sentry/utils';

import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core';
import { SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION } from '../semanticAttributes';
import type { AbstractSpan } from '../types';
import { getSpanKind } from './getSpanKind';
import { spanHasAttributes, spanHasName } from './spanTypes';
Expand Down Expand Up @@ -136,8 +137,16 @@ export function descriptionForHttpMethod(
return { op: opParts.join('.'), description: name, source: 'custom' };
}

// Ex. description="GET /api/users".
const description = `${httpMethod} ${urlPath}`;
const graphqlOperations = attributes[SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION];

// Ex. GET /api/users
const baseDescription = `${httpMethod} ${urlPath}`;

// When the http span has a graphql operation, append it to the description
// We add these in the graphqlIntegration
const description = graphqlOperations
? `${baseDescription} (${getGraphqlOperationNames(graphqlOperations)})`
: baseDescription;

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

function getGraphqlOperationNames(attr: AttributeValue): string {
if (Array.isArray(attr)) {
const sorted = attr.slice().sort();

// Up to 5 items, we just add all of them
if (sorted.length < 5) {
return sorted.join(', ');
} else {
// Else, we add the first 5 and the diff of other operations
return `${sorted.slice(0, 5).join(', ')}, +${sorted.length - 5}`;
}
}

return `${attr}`;
}

/** Exported for tests only */
export function getSanitizedUrl(
attributes: Attributes,
Expand Down

0 comments on commit 46fb889

Please sign in to comment.