From 2570f5dc3faac97e48334aaf46e1d0c15d9a1123 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 28 Jul 2026 12:14:19 +0200 Subject: [PATCH] ref(node): Remove vendored GraphQL instrumentation This instrumentation is dead code: the GraphQL integration is provided by the channel-based implementation in `@sentry/server-utils` (default since #22501), and the vendored OpenTelemetry instrumentation here is no longer wired up. Ref: #22346 Co-Authored-By: Claude Opus 4.8 --- .../src/integrations/tracing/graphql/index.ts | 48 -- .../tracing/graphql/vendored/enum.ts | 48 -- .../graphql/vendored/enums/AttributeNames.ts | 18 - .../tracing/graphql/vendored/graphql-types.ts | 26 - .../graphql/vendored/instrumentation.ts | 478 ------------------ .../graphql/vendored/internal-types.ts | 106 ---- .../tracing/graphql/vendored/symbols.ts | 12 - .../tracing/graphql/vendored/types.ts | 48 -- .../tracing/graphql/vendored/utils.ts | 372 -------------- .../tracing/graphql/addSpanSource.test.ts | 53 -- 10 files changed, 1209 deletions(-) delete mode 100644 packages/node/src/integrations/tracing/graphql/index.ts delete mode 100644 packages/node/src/integrations/tracing/graphql/vendored/enum.ts delete mode 100644 packages/node/src/integrations/tracing/graphql/vendored/enums/AttributeNames.ts delete mode 100644 packages/node/src/integrations/tracing/graphql/vendored/graphql-types.ts delete mode 100644 packages/node/src/integrations/tracing/graphql/vendored/instrumentation.ts delete mode 100644 packages/node/src/integrations/tracing/graphql/vendored/internal-types.ts delete mode 100644 packages/node/src/integrations/tracing/graphql/vendored/symbols.ts delete mode 100644 packages/node/src/integrations/tracing/graphql/vendored/types.ts delete mode 100644 packages/node/src/integrations/tracing/graphql/vendored/utils.ts delete mode 100644 packages/node/test/integrations/tracing/graphql/addSpanSource.test.ts diff --git a/packages/node/src/integrations/tracing/graphql/index.ts b/packages/node/src/integrations/tracing/graphql/index.ts deleted file mode 100644 index 95c034e07ca0..000000000000 --- a/packages/node/src/integrations/tracing/graphql/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { GraphQLInstrumentation } from './vendored/instrumentation'; -import { generateInstrumentOnce } from '../../../otel/instrument'; - -interface GraphqlOptions { - /** - * Do not create spans for resolvers. - * - * Defaults to true. - */ - ignoreResolveSpans?: boolean; - - /** - * Don't create spans for the execution of the default resolver on object properties. - * - * When a resolver function is not defined on the schema for a field, graphql will - * use the default resolver which just looks for a property with that name on the object. - * If the property is not a function, it's not very interesting to trace. - * This option can reduce noise and number of spans created. - * - * Defaults to true. - */ - ignoreTrivialResolveSpans?: boolean; - - /** - * If this is enabled, a http.server root span containing this span will automatically be renamed to include the operation name. - * Set this to `false` if you do not want this behavior, and want to keep the default http.server span name. - * - * Defaults to true. - */ - useOperationNameForRootSpan?: boolean; -} - -const INTEGRATION_NAME = 'Graphql' as const; - -export const instrumentGraphql = generateInstrumentOnce( - INTEGRATION_NAME, - GraphQLInstrumentation, - (_options: GraphqlOptions) => getOptionsWithDefaults(_options), -); - -function getOptionsWithDefaults(options?: GraphqlOptions): GraphqlOptions { - return { - ignoreResolveSpans: true, - ignoreTrivialResolveSpans: true, - useOperationNameForRootSpan: true, - ...options, - }; -} diff --git a/packages/node/src/integrations/tracing/graphql/vendored/enum.ts b/packages/node/src/integrations/tracing/graphql/vendored/enum.ts deleted file mode 100644 index 2e9fc3468168..000000000000 --- a/packages/node/src/integrations/tracing/graphql/vendored/enum.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-graphql - * - Upstream version: @opentelemetry/instrumentation-graphql@0.66.0 - */ - -export enum AllowedOperationTypes { - QUERY = 'query', - MUTATION = 'mutation', - SUBSCRIPTION = 'subscription', -} - -export enum TokenKind { - SOF = '', - EOF = '', - BANG = '!', - DOLLAR = '$', - AMP = '&', - PAREN_L = '(', - PAREN_R = ')', - SPREAD = '...', - COLON = ':', - EQUALS = '=', - AT = '@', - BRACKET_L = '[', - BRACKET_R = ']', - BRACE_L = '{', - PIPE = '|', - BRACE_R = '}', - NAME = 'Name', - INT = 'Int', - FLOAT = 'Float', - STRING = 'String', - BLOCK_STRING = 'BlockString', - COMMENT = 'Comment', -} - -export enum SpanNames { - EXECUTE = 'graphql.execute', - PARSE = 'graphql.parse', - RESOLVE = 'graphql.resolve', - VALIDATE = 'graphql.validate', - SCHEMA_VALIDATE = 'graphql.validateSchema', - SCHEMA_PARSE = 'graphql.parseSchema', -} diff --git a/packages/node/src/integrations/tracing/graphql/vendored/enums/AttributeNames.ts b/packages/node/src/integrations/tracing/graphql/vendored/enums/AttributeNames.ts deleted file mode 100644 index 19fbe9964748..000000000000 --- a/packages/node/src/integrations/tracing/graphql/vendored/enums/AttributeNames.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-graphql - * - Upstream version: @opentelemetry/instrumentation-graphql@0.66.0 - */ - -export enum AttributeNames { - SOURCE = 'graphql.source', - FIELD_NAME = 'graphql.field.name', - FIELD_PATH = 'graphql.field.path', - FIELD_TYPE = 'graphql.field.type', - PARENT_NAME = 'graphql.parent.name', - OPERATION_TYPE = 'graphql.operation.type', - OPERATION_NAME = 'graphql.operation.name', -} diff --git a/packages/node/src/integrations/tracing/graphql/vendored/graphql-types.ts b/packages/node/src/integrations/tracing/graphql/vendored/graphql-types.ts deleted file mode 100644 index e0a2b72e64f0..000000000000 --- a/packages/node/src/integrations/tracing/graphql/vendored/graphql-types.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Single source of truth for the structural graphql types, shared with the orchestrion graphql -// integration in `@sentry/server-utils` so the two can't drift. -export type { - DefinitionNode, - DocumentNode, - ExecutionArgs, - ExecutionResult, - GraphQLError, - GraphQLFieldResolver, - GraphQLObjectType, - GraphQLOutputType, - GraphQLResolveInfo, - GraphQLSchema, - GraphQLType, - GraphQLTypeResolver, - GraphQLUnionType, - Location, - Maybe, - OperationDefinitionNode, - ParseOptions, - PromiseOrValue, - Source, - Token, - TypeInfo, - ValidationRule, -} from '@sentry/server-utils/orchestrion'; diff --git a/packages/node/src/integrations/tracing/graphql/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/graphql/vendored/instrumentation.ts deleted file mode 100644 index e7e3a99b8b8e..000000000000 --- a/packages/node/src/integrations/tracing/graphql/vendored/instrumentation.ts +++ /dev/null @@ -1,478 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-graphql - * - Upstream version: @opentelemetry/instrumentation-graphql@0.66.0 - * - Types from `graphql` package inlined as simplified interfaces - * - Minor TypeScript strictness adjustments - * - Span lifecycle migrated from the OpenTelemetry tracer to the @sentry/core span API - * - `auto.graphql.otel.graphql` origin baked into the execute span (previously set via a Sentry responseHook) - * - The generic `responseHook` config was removed; its Sentry-specific logic (error status + root span renaming - * via `useOperationNameForRootSpan`) is now applied directly when the execution result is handled - */ - -/* oxlint-disable max-lines */ - -import { - isWrapped, - InstrumentationBase, - InstrumentationNodeModuleDefinition, - safeExecuteInTheMiddle, -} from '@opentelemetry/instrumentation'; -import { InstrumentationNodeModuleFile } from '../../InstrumentationNodeModuleFile'; -import type { - DefinitionNode, - DocumentNode, - ExecutionArgs, - ExecutionResult, - GraphQLError, - GraphQLFieldResolver, - GraphQLSchema, - GraphQLTypeResolver, - OperationDefinitionNode, - ParseOptions, - PromiseOrValue, - Source, - TypeInfo, - ValidationRule, -} from './graphql-types'; -import type { Maybe } from './graphql-types'; -import { SpanNames } from './enum'; -import { AttributeNames } from './enums/AttributeNames'; -import { OTEL_GRAPHQL_DATA_SYMBOL } from './symbols'; - -import { - type executeFunctionWithObj, - type executeArgumentsArray, - type executeType, - type parseType, - type validateType, - type OtelExecutionArgs, - type ObjectWithGraphQLData, - OPERATION_NOT_SUPPORTED, -} from './internal-types'; -import { addSpanSource, endSpan, getOperation, isPromise, wrapFieldResolver, wrapFields } from './utils'; - -import type { Span, SpanAttributeValue } from '@sentry/core'; -import { - getRootSpan, - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - spanToJSON, - startInactiveSpan, - withActiveSpan, -} from '@sentry/core'; - -import type { GraphQLInstrumentationConfig, GraphQLInstrumentationParsedConfig } from './types'; -import { SENTRY_GRAPHQL_OPERATION } from '@sentry/conventions/attributes'; - -const PACKAGE_NAME = '@sentry/instrumentation-graphql'; - -const ORIGIN = 'auto.graphql.otel.graphql'; - -const DEFAULT_CONFIG: GraphQLInstrumentationParsedConfig = { - ignoreResolveSpans: false, -}; - -const supportedVersions = ['>=14.0.0 <17']; - -export class GraphQLInstrumentation extends InstrumentationBase { - constructor(config: GraphQLInstrumentationConfig = {}) { - super(PACKAGE_NAME, SDK_VERSION, { ...DEFAULT_CONFIG, ...config }); - } - - override setConfig(config: GraphQLInstrumentationConfig = {}) { - super.setConfig({ ...DEFAULT_CONFIG, ...config }); - } - - protected init() { - const module = new InstrumentationNodeModuleDefinition('graphql', supportedVersions); - module.files.push(this._addPatchingExecute()); - module.files.push(this._addPatchingParser()); - module.files.push(this._addPatchingValidate()); - - return module; - } - - private _addPatchingExecute(): InstrumentationNodeModuleFile { - return new InstrumentationNodeModuleFile( - 'graphql/execution/execute.js', - supportedVersions, - // cannot make it work with appropriate type as execute function has 2 - //types and/cannot import function but only types - (moduleExports: any) => { - if (isWrapped(moduleExports.execute)) { - this._unwrap(moduleExports, 'execute'); - } - this._wrap(moduleExports, 'execute', this._patchExecute(moduleExports.defaultFieldResolver)); - return moduleExports; - }, - moduleExports => { - if (moduleExports) { - this._unwrap(moduleExports, 'execute'); - } - }, - ); - } - - private _addPatchingParser(): InstrumentationNodeModuleFile { - return new InstrumentationNodeModuleFile( - 'graphql/language/parser.js', - supportedVersions, - (moduleExports: { parse: parseType; [key: string]: any }) => { - if (isWrapped(moduleExports.parse)) { - this._unwrap(moduleExports, 'parse'); - } - this._wrap(moduleExports, 'parse', this._patchParse()); - return moduleExports; - }, - (moduleExports: { parse: parseType; [key: string]: any }) => { - if (moduleExports) { - this._unwrap(moduleExports, 'parse'); - } - }, - ); - } - - private _addPatchingValidate(): InstrumentationNodeModuleFile { - return new InstrumentationNodeModuleFile( - 'graphql/validation/validate.js', - supportedVersions, - moduleExports => { - if (isWrapped(moduleExports.validate)) { - this._unwrap(moduleExports, 'validate'); - } - this._wrap(moduleExports, 'validate', this._patchValidate()); - return moduleExports; - }, - moduleExports => { - if (moduleExports) { - this._unwrap(moduleExports, 'validate'); - } - }, - ); - } - - private _patchExecute(defaultFieldResolved: GraphQLFieldResolver): (original: executeType) => executeType { - const instrumentation = this; - return function execute(original) { - return function patchExecute(this: executeType): PromiseOrValue { - let processedArgs: OtelExecutionArgs; - - // case when apollo server is used for example - if (arguments.length >= 2) { - const args = arguments as unknown as executeArgumentsArray; - processedArgs = instrumentation._wrapExecuteArgs( - args[0], - args[1], - args[2], - args[3], - args[4], - args[5], - args[6], - args[7], - defaultFieldResolved, - ); - } else { - const args = arguments[0] as ExecutionArgs; - processedArgs = instrumentation._wrapExecuteArgs( - args.schema, - args.document, - args.rootValue, - args.contextValue, - args.variableValues, - args.operationName, - args.fieldResolver, - args.typeResolver, - defaultFieldResolved, - ); - } - - const operation = getOperation(processedArgs.document, processedArgs.operationName); - - const span = instrumentation._createExecuteSpan(operation, processedArgs); - - processedArgs.contextValue[OTEL_GRAPHQL_DATA_SYMBOL] = { - source: processedArgs.document - ? processedArgs.document || (processedArgs.document as ObjectWithGraphQLData)[OTEL_GRAPHQL_DATA_SYMBOL] - : undefined, - span, - fields: {}, - }; - - return withActiveSpan(span, () => { - return safeExecuteInTheMiddle>( - () => { - return (original as executeFunctionWithObj).apply(this, [processedArgs]); - }, - (err, result) => { - instrumentation._handleExecutionResult(span, err, result); - }, - ); - }); - }; - }; - } - - private _handleExecutionResult(span: Span, err?: Error, result?: PromiseOrValue) { - if (result === undefined || err) { - endSpan(span, err); - return; - } - - if (isPromise(result)) { - result.then( - resultData => { - this._updateSpanFromResult(span, resultData); - endSpan(span); - }, - error => { - endSpan(span, error); - }, - ); - } else { - this._updateSpanFromResult(span, result); - endSpan(span); - } - } - - /** - * Applies Sentry-specific span mutations based on the GraphQL execution result: - * - Marks the execute span as errored if the result contains errors (and no status was set yet) - * - Optionally renames the containing root span to include the GraphQL operation name(s) - */ - private _updateSpanFromResult(span: Span, result: ExecutionResult): void { - // We want to ensure spans are marked as errored if there are errors in the result - // We only do that if the span is not already marked with another error status - if (result.errors?.length && spanToJSON(span).status === 'ok') { - span.setStatus({ code: SPAN_STATUS_ERROR }); - } - - if (!this.getConfig().useOperationNameForRootSpan) { - return; - } - - const attributes = spanToJSON(span).data; - - // If operation.name is not set, we fall back to use operation.type only - const operationType = attributes[AttributeNames.OPERATION_TYPE]; - const operationName = attributes[AttributeNames.OPERATION_NAME]; - - if (!operationType) { - return; - } - - const rootSpan = getRootSpan(span); - const rootSpanAttributes = spanToJSON(rootSpan).data; - - const existingOperations = rootSpanAttributes[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 as string[]).push(newOperation); - rootSpan.setAttribute(SENTRY_GRAPHQL_OPERATION, existingOperations); - } else if (typeof existingOperations === 'string') { - rootSpan.setAttribute(SENTRY_GRAPHQL_OPERATION, [existingOperations, newOperation]); - } else { - rootSpan.setAttribute(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, - )})`, - ); - } - - private _patchParse(): (original: parseType) => parseType { - const instrumentation = this; - return function parse(original) { - return function patchParse(this: parseType, source: string | Source, options?: ParseOptions): DocumentNode { - return instrumentation._parse(this, original, source, options); - }; - }; - } - - private _patchValidate(): (original: validateType) => validateType { - const instrumentation = this; - return function validate(original: validateType) { - return function patchValidate( - this: validateType, - schema: GraphQLSchema, - documentAST: DocumentNode, - rules?: ReadonlyArray, - options?: { maxErrors?: number }, - typeInfo?: TypeInfo, - ): ReadonlyArray { - return instrumentation._validate(this, original, schema, documentAST, rules, typeInfo, options); - }; - }; - } - - private _parse(obj: parseType, original: parseType, source: string | Source, options?: ParseOptions): DocumentNode { - const span = startInactiveSpan({ name: SpanNames.PARSE }); - - return withActiveSpan(span, () => { - return safeExecuteInTheMiddle( - () => { - return original.call(obj, source, options); - }, - (err, result) => { - if (result) { - const operation = getOperation(result); - if (!operation) { - span.updateName(SpanNames.SCHEMA_PARSE); - } else if (result.loc) { - addSpanSource(span, result.loc); - } - } - endSpan(span, err); - }, - ); - }); - } - - private _validate( - obj: validateType, - original: validateType, - schema: GraphQLSchema, - documentAST: DocumentNode, - rules?: ReadonlyArray, - typeInfo?: TypeInfo, - options?: { maxErrors?: number }, - ): ReadonlyArray { - const span = startInactiveSpan({ name: SpanNames.VALIDATE }); - - return withActiveSpan(span, () => { - return safeExecuteInTheMiddle>( - () => { - return original.call(obj, schema, documentAST, rules, options, typeInfo); - }, - (err, _errors) => { - if (!documentAST.loc) { - span.updateName(SpanNames.SCHEMA_VALIDATE); - } - endSpan(span, err); - }, - ); - }); - } - - private _createExecuteSpan(operation: DefinitionNode | undefined, processedArgs: OtelExecutionArgs): Span { - const span = startInactiveSpan({ - name: SpanNames.EXECUTE, - attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN }, - }); - if (operation) { - const { operation: operationType, name: nameNode } = operation as OperationDefinitionNode; - - span.setAttribute(AttributeNames.OPERATION_TYPE, operationType); - - const operationName = nameNode?.value; - - // https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/instrumentation/graphql/ - // > The span name MUST be of the format provided that graphql.operation.type and graphql.operation.name are available. - // > If graphql.operation.name is not available, the span SHOULD be named . - if (operationName) { - span.setAttribute(AttributeNames.OPERATION_NAME, operationName); - span.updateName(`${operationType} ${operationName}`); - } else { - span.updateName(operationType); - } - } else { - let operationName = ' '; - if (processedArgs.operationName) { - operationName = ` "${processedArgs.operationName}" `; - } - operationName = OPERATION_NOT_SUPPORTED.replace('$operationName$', operationName); - span.setAttribute(AttributeNames.OPERATION_NAME, operationName); - } - - if (processedArgs.document?.loc) { - addSpanSource(span, processedArgs.document.loc); - } - - return span; - } - - private _wrapExecuteArgs( - schema: GraphQLSchema, - document: DocumentNode, - rootValue: any, - contextValue: any, - variableValues: Maybe<{ [key: string]: any }>, - operationName: Maybe, - fieldResolver: Maybe>, - typeResolver: Maybe>, - defaultFieldResolved: GraphQLFieldResolver, - ): OtelExecutionArgs { - if (!contextValue) { - // oxlint-disable-next-line no-param-reassign - contextValue = {}; - } - - if (contextValue[OTEL_GRAPHQL_DATA_SYMBOL] || this.getConfig().ignoreResolveSpans) { - return { - schema, - document, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - typeResolver, - }; - } - - const isUsingDefaultResolver = fieldResolver == null; - // follows graphql implementation here: - // https://github.com/graphql/graphql-js/blob/0b7daed9811731362c71900e12e5ea0d1ecc7f1f/src/execution/execute.ts#L494 - const fieldResolverForExecute = fieldResolver ?? defaultFieldResolved; - // oxlint-disable-next-line no-param-reassign - fieldResolver = wrapFieldResolver(() => this.getConfig(), fieldResolverForExecute, isUsingDefaultResolver); - - if (schema) { - wrapFields(schema.getQueryType() as any, () => this.getConfig()); - wrapFields(schema.getMutationType() as any, () => this.getConfig()); - } - - return { - schema, - document, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - typeResolver, - }; - } -} - -// copy from packages/opentelemetry/utils -function getGraphqlOperationNamesFromAttribute(attr: SpanAttributeValue): string { - if (Array.isArray(attr)) { - // oxlint-disable-next-line typescript/require-array-sort-compare - 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}`; -} diff --git a/packages/node/src/integrations/tracing/graphql/vendored/internal-types.ts b/packages/node/src/integrations/tracing/graphql/vendored/internal-types.ts deleted file mode 100644 index e206200e3ffb..000000000000 --- a/packages/node/src/integrations/tracing/graphql/vendored/internal-types.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-graphql - * - Upstream version: @opentelemetry/instrumentation-graphql@0.66.0 - * - Types from `graphql` package inlined as simplified interfaces - * - Minor TypeScript strictness adjustments - */ - -import type { Span } from '@sentry/core'; -import type { - DocumentNode, - ExecutionArgs, - ExecutionResult, - GraphQLError, - GraphQLFieldResolver, - GraphQLSchema, - GraphQLTypeResolver, - Maybe, - ParseOptions, - PromiseOrValue, - Source, - TypeInfo, - ValidationRule, -} from './graphql-types'; -import type { OTEL_GRAPHQL_DATA_SYMBOL, OTEL_PATCHED_SYMBOL } from './symbols'; - -export type { Maybe } from './graphql-types'; - -export const OPERATION_NOT_SUPPORTED = 'Operation$operationName$not' + ' supported'; - -export type executeFunctionWithObj = (args: ExecutionArgs) => PromiseOrValue; - -export type executeArgumentsArray = [ - GraphQLSchema, - DocumentNode, - any, - any, - Maybe<{ [key: string]: any }>, - Maybe, - Maybe>, - Maybe>, -]; - -export type executeFunctionWithArgs = ( - schema: GraphQLSchema, - document: DocumentNode, - rootValue?: any, - contextValue?: any, - variableValues?: Maybe<{ [key: string]: any }>, - operationName?: Maybe, - fieldResolver?: Maybe>, - typeResolver?: Maybe>, -) => PromiseOrValue; - -export interface OtelExecutionArgs { - schema: GraphQLSchema; - document: DocumentNode & ObjectWithGraphQLData; - rootValue?: any; - contextValue?: any & ObjectWithGraphQLData; - variableValues?: Maybe<{ [key: string]: any }>; - operationName?: Maybe; - fieldResolver?: Maybe & OtelPatched>; - typeResolver?: Maybe>; -} - -export type executeType = executeFunctionWithObj | executeFunctionWithArgs; - -export type parseType = (source: string | Source, options?: ParseOptions) => DocumentNode; - -export type validateType = ( - schema: GraphQLSchema, - documentAST: DocumentNode, - rules?: ReadonlyArray, - options?: { maxErrors?: number }, - typeInfo?: TypeInfo, -) => ReadonlyArray; - -export interface GraphQLField { - span: Span; -} - -interface OtelGraphQLData { - source?: any; - span: Span; - fields: { [key: string]: GraphQLField }; -} - -export interface ObjectWithGraphQLData { - [OTEL_GRAPHQL_DATA_SYMBOL]?: OtelGraphQLData; -} - -export interface OtelPatched { - [OTEL_PATCHED_SYMBOL]?: boolean; -} - -export interface GraphQLPath { - prev: GraphQLPath | undefined; - key: string | number; - /** - * optional as it didn't exist yet in ver 14 - */ - typename?: string | undefined; -} diff --git a/packages/node/src/integrations/tracing/graphql/vendored/symbols.ts b/packages/node/src/integrations/tracing/graphql/vendored/symbols.ts deleted file mode 100644 index 064363ac48ba..000000000000 --- a/packages/node/src/integrations/tracing/graphql/vendored/symbols.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-graphql - * - Upstream version: @opentelemetry/instrumentation-graphql@0.66.0 - */ - -export const OTEL_PATCHED_SYMBOL = Symbol.for('opentelemetry.patched'); - -export const OTEL_GRAPHQL_DATA_SYMBOL = Symbol.for('opentelemetry.graphql_data'); diff --git a/packages/node/src/integrations/tracing/graphql/vendored/types.ts b/packages/node/src/integrations/tracing/graphql/vendored/types.ts deleted file mode 100644 index cbae5ba771f0..000000000000 --- a/packages/node/src/integrations/tracing/graphql/vendored/types.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-graphql - * - Upstream version: @opentelemetry/instrumentation-graphql@0.66.0 - */ - -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; - -export interface GraphQLInstrumentationConfig extends InstrumentationConfig { - /** - * Do not create spans for resolvers. - * - * @default false - */ - ignoreResolveSpans?: boolean; - - /** - * Don't create spans for the execution of the default resolver on object properties. - * - * When a resolver function is not defined on the schema for a field, graphql will - * use the default resolver which just looks for a property with that name on the object. - * If the property is not a function, it's not very interesting to trace. - * This option can reduce noise and number of spans created. - * - * @default false - */ - ignoreTrivialResolveSpans?: boolean; - - /** - * If this is enabled, a `http.server` root span containing the execute span will automatically be renamed - * to include the operation name. - * - * @default false - */ - useOperationNameForRootSpan?: boolean; -} - -// Utility type to make specific properties required -type RequireSpecificKeys = T & { [P in K]-?: T[P] }; - -// Merged and parsed config of default instrumentation config and GraphQL -export type GraphQLInstrumentationParsedConfig = RequireSpecificKeys< - GraphQLInstrumentationConfig, - 'ignoreResolveSpans' ->; diff --git a/packages/node/src/integrations/tracing/graphql/vendored/utils.ts b/packages/node/src/integrations/tracing/graphql/vendored/utils.ts deleted file mode 100644 index ce3562e67d35..000000000000 --- a/packages/node/src/integrations/tracing/graphql/vendored/utils.ts +++ /dev/null @@ -1,372 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - * - * NOTICE from the Sentry authors: - * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-graphql - * - Upstream version: @opentelemetry/instrumentation-graphql@0.66.0 - * - Types from `graphql` package inlined as simplified interfaces - * - Minor TypeScript strictness adjustments - * - Span lifecycle migrated from the OpenTelemetry tracer to the @sentry/core span API - */ - -import type { - DocumentNode, - GraphQLFieldResolver, - GraphQLObjectType, - GraphQLOutputType, - GraphQLResolveInfo, - GraphQLType, - GraphQLUnionType, - Location, - Maybe, - Token, -} from './graphql-types'; -import type { Span, SpanAttributes } from '@sentry/core'; -import { getClient, isObjectLike, SPAN_STATUS_ERROR, startInactiveSpan, withActiveSpan } from '@sentry/core'; -import { AllowedOperationTypes, SpanNames, TokenKind } from './enum'; -import { AttributeNames } from './enums/AttributeNames'; -import { OTEL_GRAPHQL_DATA_SYMBOL, OTEL_PATCHED_SYMBOL } from './symbols'; -import type { GraphQLField, GraphQLPath, ObjectWithGraphQLData, OtelPatched } from './internal-types'; -import type { GraphQLInstrumentationParsedConfig } from './types'; - -const OPERATION_VALUES = Object.values(AllowedOperationTypes); - -// https://github.com/graphql/graphql-js/blob/main/src/jsutils/isPromise.ts -export const isPromise = (value: any): value is Promise => { - return typeof value?.then === 'function'; -}; - -export function addSpanSource(span: Span, loc?: Location, start?: number, end?: number): void { - if (getClient()?.getDataCollectionOptions().graphQL.document === true) { - const source = getSourceFromLocation(loc, start, end); - span.setAttribute(AttributeNames.SOURCE, source); - } -} - -function createFieldIfNotExists( - contextValue: any, - info: GraphQLResolveInfo, - path: string[], -): { - field: GraphQLField; - spanAdded: boolean; -} { - let field = getField(contextValue, path); - if (field) { - return { field, spanAdded: false }; - } - - const parentSpan = getParentFieldSpan(contextValue, path); - - field = { - span: createResolverSpan(contextValue, info, path, parentSpan), - }; - - addField(contextValue, path, field); - - return { field, spanAdded: true }; -} - -function createResolverSpan(contextValue: any, info: GraphQLResolveInfo, path: string[], parentSpan?: Span): Span { - const attributes: SpanAttributes = { - [AttributeNames.FIELD_NAME]: info.fieldName, - [AttributeNames.FIELD_PATH]: path.join('.'), - [AttributeNames.FIELD_TYPE]: info.returnType.toString(), - [AttributeNames.PARENT_NAME]: info.parentType.name, - }; - - const span = startInactiveSpan({ - name: `${SpanNames.RESOLVE} ${attributes[AttributeNames.FIELD_PATH]}`, - attributes, - parentSpan, - }); - - const document = contextValue[OTEL_GRAPHQL_DATA_SYMBOL].source; - const fieldNode = info.fieldNodes.find(fieldNode => fieldNode.kind === 'Field'); - - if (fieldNode) { - addSpanSource(span, document.loc, fieldNode.loc?.start, fieldNode.loc?.end); - } - - return span; -} - -export function endSpan(span: Span, error?: Error): void { - if (error) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: error.message }); - } - span.end(); -} - -export function getOperation(document: DocumentNode, operationName?: Maybe): DefinitionNodeLike | undefined { - if (!document || !Array.isArray(document.definitions)) { - return undefined; - } - - if (operationName) { - return document.definitions - .filter(definition => OPERATION_VALUES.indexOf(definition?.operation) !== -1) - .find(definition => operationName === definition?.name?.value); - } else { - return document.definitions.find(definition => OPERATION_VALUES.indexOf(definition?.operation) !== -1); - } -} - -type DefinitionNodeLike = DocumentNode['definitions'][number]; - -function addField(contextValue: any, path: string[], field: GraphQLField) { - return (contextValue[OTEL_GRAPHQL_DATA_SYMBOL].fields[path.join('.')] = field); -} - -function getField(contextValue: any, path: string[]): GraphQLField { - return contextValue[OTEL_GRAPHQL_DATA_SYMBOL].fields[path.join('.')]; -} - -function getParentFieldSpan(contextValue: any, path: string[]): Span { - for (let i = path.length - 1; i > 0; i--) { - const field = getField(contextValue, path.slice(0, i)); - - if (field) { - return field.span; - } - } - - return getRootSpan(contextValue); -} - -function getRootSpan(contextValue: any): Span { - return contextValue[OTEL_GRAPHQL_DATA_SYMBOL].span; -} - -function pathToArray(path: GraphQLPath): string[] { - const flattened: string[] = []; - let curr: GraphQLPath | undefined = path; - while (curr) { - flattened.push(String(curr.key)); - curr = curr.prev; - } - return flattened.reverse(); -} - -function repeatBreak(i: number): string { - return repeatChar('\n', i); -} - -function repeatSpace(i: number): string { - return repeatChar(' ', i); -} - -function repeatChar(char: string, to: number): string { - let text = ''; - for (let i = 0; i < to; i++) { - text += char; - } - return text; -} - -const KindsToBeRemoved: string[] = [TokenKind.FLOAT, TokenKind.STRING, TokenKind.INT, TokenKind.BLOCK_STRING]; - -export function getSourceFromLocation(loc?: Location, inputStart?: number, inputEnd?: number): string { - let source = ''; - - if (loc?.startToken) { - const start = typeof inputStart === 'number' ? inputStart : loc.start; - const end = typeof inputEnd === 'number' ? inputEnd : loc.end; - - let next: Token | null = loc.startToken.next; - let previousLine: number | undefined = 1; - while (next) { - if (next.start < start) { - next = next.next; - previousLine = next?.line; - continue; - } - if (next.end > end) { - next = next.next; - previousLine = next?.line; - continue; - } - let value = next.value || next.kind; - let space = ''; - if (KindsToBeRemoved.indexOf(next.kind) >= 0) { - value = '*'; - } - if (next.kind === TokenKind.STRING) { - value = `"${value}"`; - } - if (next.kind === TokenKind.EOF) { - value = ''; - } - if (next.line > previousLine!) { - source += repeatBreak(next.line - previousLine!); - previousLine = next.line; - space = repeatSpace(next.column - 1); - } else { - if (next.line === next.prev?.line) { - space = repeatSpace(next.start - (next.prev?.end || 0)); - } - } - source += space + value; - if (next) { - next = next.next!; - } - } - } - - return source; -} - -export function wrapFields( - type: Maybe, - getConfig: () => GraphQLInstrumentationParsedConfig, -): void { - if (!type || (type as any)[OTEL_PATCHED_SYMBOL]) { - return; - } - const fields = type.getFields(); - - (type as any)[OTEL_PATCHED_SYMBOL] = true; - - Object.keys(fields).forEach(key => { - const field = fields[key]; - - if (!field) { - return; - } - - if (field.resolve) { - // The shared structural types narrow the resolver context to `ObjectWithGraphQLData`; cast back - // to the field's own resolver type (behavior is unchanged — this is a type-only adjustment). - field.resolve = wrapFieldResolver(getConfig, field.resolve) as GraphQLFieldResolver; - } - - if (field.type) { - const unwrappedTypes = unwrapType(field.type); - for (const unwrappedType of unwrappedTypes) { - wrapFields(unwrappedType as any, getConfig); - } - } - }); -} - -function unwrapType(type: GraphQLOutputType): readonly GraphQLObjectType[] { - // unwrap wrapping types (non-nullable and list types) - if ('ofType' in type) { - // The structural index signature widens `ofType` to `unknown`, so narrow it back explicitly. - return unwrapType(type.ofType as GraphQLOutputType); - } - - // unwrap union types - if (isGraphQLUnionType(type)) { - return type.getTypes(); - } - - // return object types - if (isGraphQLObjectType(type)) { - return [type]; - } - - return []; -} - -function isGraphQLUnionType(type: GraphQLType): type is GraphQLUnionType { - return 'getTypes' in type && typeof type.getTypes === 'function'; -} - -function isGraphQLObjectType(type: GraphQLType): type is GraphQLObjectType { - return 'getFields' in type && typeof type.getFields === 'function'; -} - -const handleResolveSpanError = (resolveSpan: Span, err: any, shouldEndSpan: boolean) => { - if (!shouldEndSpan) { - return; - } - resolveSpan.setStatus({ - code: SPAN_STATUS_ERROR, - message: err.message, - }); - resolveSpan.end(); -}; - -const handleResolveSpanSuccess = (resolveSpan: Span, shouldEndSpan: boolean) => { - if (!shouldEndSpan) { - return; - } - resolveSpan.end(); -}; - -export function wrapFieldResolver( - getConfig: () => GraphQLInstrumentationParsedConfig, - fieldResolver: Maybe & OtelPatched>, - isDefaultResolver = false, -): GraphQLFieldResolver & OtelPatched { - if ((wrappedFieldResolver as OtelPatched)[OTEL_PATCHED_SYMBOL] || typeof fieldResolver !== 'function') { - return fieldResolver!; - } - - function wrappedFieldResolver( - this: GraphQLFieldResolver, - source: TSource, - args: TArgs, - contextValue: TContext & ObjectWithGraphQLData, - info: GraphQLResolveInfo, - ) { - if (!fieldResolver) { - return undefined; - } - const config = getConfig(); - - // follows what graphql is doing to decide if this is a trivial resolver - // for which we don't need to create a resolve span - if ( - config.ignoreTrivialResolveSpans && - isDefaultResolver && - (isObjectLike(source) || typeof source === 'function') - ) { - const property = (source as any)[info.fieldName]; - // a function execution is not trivial and should be recorder. - // property which is not a function is just a value and we don't want a "resolve" span for it - if (typeof property !== 'function') { - return fieldResolver.call(this, source, args, contextValue, info); - } - } - - if (!contextValue[OTEL_GRAPHQL_DATA_SYMBOL]) { - return fieldResolver.call(this, source, args, contextValue, info); - } - const path = pathToArray(info?.path); - - const { field, spanAdded } = createFieldIfNotExists(contextValue, info, path); - const span = field.span; - const shouldEndSpan = spanAdded; - - return withActiveSpan(span, () => { - try { - const res = fieldResolver.call(this, source, args, contextValue, info); - if (isPromise(res)) { - return res.then( - (r: any) => { - handleResolveSpanSuccess(span, shouldEndSpan); - return r; - }, - (err: Error) => { - handleResolveSpanError(span, err, shouldEndSpan); - throw err; - }, - ); - } else { - handleResolveSpanSuccess(span, shouldEndSpan); - return res; - } - } catch (err: any) { - handleResolveSpanError(span, err, shouldEndSpan); - throw err; - } - }); - } - - (wrappedFieldResolver as OtelPatched)[OTEL_PATCHED_SYMBOL] = true; - - return wrappedFieldResolver; -} diff --git a/packages/node/test/integrations/tracing/graphql/addSpanSource.test.ts b/packages/node/test/integrations/tracing/graphql/addSpanSource.test.ts deleted file mode 100644 index 7df7df97ead1..000000000000 --- a/packages/node/test/integrations/tracing/graphql/addSpanSource.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import * as core from '@sentry/core'; -import { addSpanSource } from '../../../../src/integrations/tracing/graphql/vendored/utils'; -import type { Location, Token } from '@sentry/server-utils/orchestrion'; - -vi.spyOn(core, 'getClient'); - -function mockClient(graphQLDocument: boolean): void { - vi.mocked(core.getClient).mockReturnValue({ - getDataCollectionOptions: () => ({ graphQL: { document: graphQLDocument, variables: true } }), - } as any); -} - -function makeLocation(): Location { - const token: Token = { - kind: 'Name', - start: 0, - end: 5, - line: 1, - column: 1, - value: 'hello', - prev: null, - next: null, - }; - return { start: 0, end: 5, startToken: token }; -} - -describe('addSpanSource dataCollection gate', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('sets graphql.source attribute when graphQL.document is true', () => { - mockClient(true); - const span = { setAttribute: vi.fn() } as any; - addSpanSource(span, makeLocation()); - expect(span.setAttribute).toHaveBeenCalledWith('graphql.source', expect.any(String)); - }); - - it('does not set attribute when graphQL.document is false', () => { - mockClient(false); - const span = { setAttribute: vi.fn() } as any; - addSpanSource(span, makeLocation()); - expect(span.setAttribute).not.toHaveBeenCalled(); - }); - - it('does not set attribute when getClient() is undefined', () => { - vi.mocked(core.getClient).mockReturnValue(undefined); - const span = { setAttribute: vi.fn() } as any; - addSpanSource(span, makeLocation()); - expect(span.setAttribute).not.toHaveBeenCalled(); - }); -});