diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/apollo-server.js b/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/apollo-server.js new file mode 100644 index 000000000000..6561adaf67ce --- /dev/null +++ b/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/apollo-server.js @@ -0,0 +1,35 @@ +const { ApolloServer, gql } = require('apollo-server'); +const Sentry = require('@sentry/aws-serverless'); + +module.exports = () => { + return Sentry.startSpan({ name: 'Test Server Start' }, () => { + return new ApolloServer({ + typeDefs: gql` + type Query { + hello: String + world: String + } + type Mutation { + login(email: String): String + } + `, + resolvers: { + Query: { + hello: () => { + return 'Hello!'; + }, + world: () => { + return 'World!'; + }, + }, + Mutation: { + login: async (_, { email }) => { + return `${email}--token`; + }, + }, + }, + introspection: false, + debug: false, + }); + }); +}; diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/scenario.js b/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/scenario.js new file mode 100644 index 000000000000..4023421921b5 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/scenario.js @@ -0,0 +1,27 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/aws-serverless'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, + integrations: [Sentry.graphqlIntegration({ useOperationNameForRootSpan: true })], + transport: loggingTransport, +}); + +async function run() { + const apolloServer = require('./apollo-server')(); + + await Sentry.startSpan({ name: 'Test Transaction' }, async span => { + // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation + await apolloServer.executeOperation({ + query: 'query GetHello {hello}', + }); + + setTimeout(() => { + span.end(); + apolloServer.stop(); + }, 500); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/test.ts new file mode 100644 index 000000000000..84098edb46ae --- /dev/null +++ b/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/test.ts @@ -0,0 +1,28 @@ +import { afterAll, describe, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +const EXPECTED_TRANSCATION = { + transaction: 'Test Transaction (query GetHello)', + spans: expect.arrayContaining([ + expect.objectContaining({ + description: 'query GetHello', + origin: 'auto.graphql.otel.graphql', + status: 'ok', + }), + ]), +}; + +describe('graphqlIntegration', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should use GraphQL operation name for root span if useOperationNameForRootSpan is set', async () => { + await createRunner(__dirname, 'scenario.js') + .ignore('event') + .expect({ transaction: { transaction: 'Test Server Start (query IntrospectionQuery)' } }) + .expect({ transaction: EXPECTED_TRANSCATION }) + .start() + .completed(); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts index c9289efbde8e..2abe2932ece2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts @@ -3,13 +3,13 @@ import { createRunner } from '../../../utils/runner'; // Graphql Instrumentation emits some spans by default on server start const EXPECTED_START_SERVER_TRANSACTION = { - transaction: 'Test Server Start', + transaction: 'Test Server Start (query IntrospectionQuery)', }; describe('GraphQL/Apollo Tests', () => { test('should instrument GraphQL queries used from Apollo Server.', async () => { const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction', + transaction: 'Test Transaction (query)', spans: expect.arrayContaining([ expect.objectContaining({ data: { @@ -33,7 +33,7 @@ describe('GraphQL/Apollo Tests', () => { test('should instrument GraphQL mutations used from Apollo Server.', async () => { const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction', + transaction: 'Test Transaction (mutation Mutation)', spans: expect.arrayContaining([ expect.objectContaining({ data: { diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/test.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/test.ts index 4aa7616cc73c..b77dcd34777b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/test.ts @@ -1,9 +1,9 @@ import { createRunner } from '../../../../utils/runner'; -import { describe, test, expect } from 'vitest' +import { describe, test, expect } from 'vitest'; // Graphql Instrumentation emits some spans by default on server start const EXPECTED_START_SERVER_TRANSACTION = { - transaction: 'Test Server Start', + transaction: 'Test Server Start (query IntrospectionQuery)', }; describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => { @@ -61,7 +61,7 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => { test('useOperationNameForRootSpan ignores an invalid root span', async () => { const EXPECTED_TRANSACTION = { - transaction: 'test span name', + transaction: 'test span name (query GetHello)', spans: expect.arrayContaining([ expect.objectContaining({ data: { diff --git a/packages/node/src/integrations/tracing/graphql.ts b/packages/node/src/integrations/tracing/graphql.ts index 945327064df2..c2316eb4ac8f 100644 --- a/packages/node/src/integrations/tracing/graphql.ts +++ b/packages/node/src/integrations/tracing/graphql.ts @@ -5,6 +5,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION } from '@sentry/opentelemet import { generateInstrumentOnce } from '../../otel/instrument'; import { addOriginToSpan } from '../../utils/addOriginToSpan'; +import type { AttributeValue } from '@opentelemetry/api'; interface GraphqlOptions { /** @@ -71,6 +72,16 @@ export const instrumentGraphql = generateInstrumentOnce( } else { rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION, newOperation); } + + if (!spanToJSON(rootSpan).data['original-description']) { + rootSpan.setAttribute('original-description', spanToJSON(rootSpan).description); + } + // Important for e.g. @sentry/aws-serverless because this would otherwise overwrite the name again + rootSpan.updateName( + `${spanToJSON(rootSpan).data['original-description']} (${getGraphqlOperationNamesFromAttribute( + existingOperations, + )})`, + ); } }, }); @@ -114,3 +125,20 @@ function getOptionsWithDefaults(options?: GraphqlOptions): GraphqlOptions { ...options, }; } + +// copy from packages/opentelemetry/utils +function getGraphqlOperationNamesFromAttribute(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}`; +}