From c8e7d55306eb3d1087a0c20546bdbaa81d1ae363 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 12 Aug 2026 10:08:31 +0200 Subject: [PATCH] ref(server-utils): Streamline graphql integration --- packages/deno/src/index.ts | 2 +- packages/deno/src/sdk.ts | 4 +- packages/node/src/index.ts | 2 +- .../node/src/integrations/tracing/index.ts | 4 +- packages/server-utils/src/graphql/index.ts | 30 ----------- packages/server-utils/src/index.ts | 1 - .../graphql/graphql-dc-subscriber.ts | 8 +-- .../src/integrations/graphql/index.ts | 52 ++++++++----------- .../src/integrations/graphql/spans.ts | 9 +--- .../src/integrations/graphql/types.ts | 16 ++++++ .../src/{ => integrations}/graphql/utils.ts | 17 +----- .../server-utils/src/orchestrion/index.ts | 5 +- .../server-utils/test/graphql/utils.test.ts | 4 +- 13 files changed, 55 insertions(+), 99 deletions(-) delete mode 100644 packages/server-utils/src/graphql/index.ts rename packages/server-utils/src/{ => integrations}/graphql/graphql-dc-subscriber.ts (97%) rename packages/server-utils/src/{ => integrations}/graphql/utils.ts (93%) diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 2cb5bd3bbd07..2c3339183d05 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -126,7 +126,7 @@ export { firebaseIntegration, genericPoolIntegration, googleGenAIIntegration, - graphqlDiagnosticsIntegration, + graphqlIntegration, hapiIntegration, kafkajsIntegration, knexIntegration, diff --git a/packages/deno/src/sdk.ts b/packages/deno/src/sdk.ts index dd620794fbc4..a68215ff6e59 100644 --- a/packages/deno/src/sdk.ts +++ b/packages/deno/src/sdk.ts @@ -19,7 +19,7 @@ import { firebaseIntegration, genericPoolIntegration, googleGenAIIntegration, - graphqlDiagnosticsIntegration, + graphqlIntegration, hapiIntegration, kafkajsIntegration, koaIntegration, @@ -65,7 +65,7 @@ export function getDefaultIntegrations(_options: Options): Integration[] { denoServeIntegration(), denoHttpIntegration(), redisIntegration(), - graphqlDiagnosticsIntegration(), + graphqlIntegration(), vercelAIIntegration(), // orchestrion-based instrumentations. We add a deliberate list here rather // than every channel integration: each one needs a Deno test proving it diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index b1298e4d96f4..0cf4712b42fe 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -11,7 +11,7 @@ export { firebaseIntegration, genericPoolIntegration, googleGenAIIntegration, - graphqlDiagnosticsIntegration as graphqlIntegration, + graphqlIntegration, hapiIntegration, kafkajsIntegration as kafkaIntegration, knexIntegration, diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index 0e881294223f..f1a2ca7131b4 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -7,7 +7,7 @@ import { firebaseIntegration, genericPoolIntegration, googleGenAIIntegration, - graphqlDiagnosticsIntegration, + graphqlIntegration, hapiIntegration, kafkajsIntegration, koaIntegration, @@ -31,7 +31,7 @@ export function getAutoPerformanceIntegrations(): Integration[] { return [ expressIntegration(), fastifyIntegration(), - graphqlDiagnosticsIntegration(), + graphqlIntegration(), mongodbIntegration(), mongooseIntegration(), mysqlIntegration(), diff --git a/packages/server-utils/src/graphql/index.ts b/packages/server-utils/src/graphql/index.ts deleted file mode 100644 index 6eb186ac23c8..000000000000 --- a/packages/server-utils/src/graphql/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { defineIntegration, type IntegrationFn, waitForTracingChannelBinding } from '@sentry/core'; -import * as dc from 'node:diagnostics_channel'; -import { type GraphqlDiagnosticChannelsOptions, subscribeGraphqlDiagnosticChannels } from './graphql-dc-subscriber'; - -const _graphqlIntegration = ((options: GraphqlDiagnosticChannelsOptions = {}) => { - return { - name: 'Graphql' as const, - setupOnce() { - // Bail on Node <= 18.18.0, where `tracingChannel` does not exist. - if (!dc.tracingChannel) { - return; - } - - // Subscribe to graphql's native tracing channels (graphql >= 17). - // This is a no-op on versions that don't publish to the channels, so it is always safe to call. - waitForTracingChannelBinding(() => { - subscribeGraphqlDiagnosticChannels(dc.tracingChannel, options); - }); - }, - }; -}) satisfies IntegrationFn; - -/** - * Auto-instrument the [graphql](https://www.npmjs.com/package/graphql) library via its native - * `node:diagnostics_channel` tracing channels (graphql >= 17). - * - * On older graphql versions the channels are never published to, so this integration is inert and - * the vendored OTel instrumentation (gated to `< 17`) handles instrumentation instead. - */ -export const graphqlIntegration = defineIntegration(_graphqlIntegration); diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index bb9a46079c82..c83a48ca8e15 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -1,7 +1,6 @@ export * from './exports'; // Exports using diagnostics channels -export { graphqlIntegration } from './graphql'; export { mongooseIntegration } from './mongoose'; export { mysql2Integration } from './mysql2'; export { instrumentPrisma, prismaIntegration } from './prisma'; diff --git a/packages/server-utils/src/graphql/graphql-dc-subscriber.ts b/packages/server-utils/src/integrations/graphql/graphql-dc-subscriber.ts similarity index 97% rename from packages/server-utils/src/graphql/graphql-dc-subscriber.ts rename to packages/server-utils/src/integrations/graphql/graphql-dc-subscriber.ts index cd4337954dc4..72c35b4b2129 100644 --- a/packages/server-utils/src/graphql/graphql-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/graphql/graphql-dc-subscriber.ts @@ -7,8 +7,8 @@ import { SPAN_STATUS_ERROR, startInactiveSpan, } from '@sentry/core'; -import { bindTracingChannelToSpan } from '../tracing-channel'; -import type { GraphqlDocumentNode } from './utils'; +import { bindTracingChannelToSpan } from '../../tracing-channel'; +import type { GraphqlDocumentNode } from './types'; import { collectGraphqlDocument, getOperationSpanName, hasResultErrors, renameRootSpanWithOperation } from './utils'; // Channel names published by graphql >= 17.0.0 (see graphql-js `src/diagnostics.ts`). @@ -85,7 +85,7 @@ export interface GraphqlResolveData { } /** Options controlling which graphql channels the subscriber emits spans for. */ -export interface GraphqlDiagnosticChannelsOptions { +export interface GraphQLOptions { /** * Do not create spans for resolvers. Resolver spans are per-field and can be very high volume. * Defaults to `true`. @@ -128,7 +128,7 @@ export type GraphqlTracingChannelFactory = (name: string) => T */ export function subscribeGraphqlDiagnosticChannels( tracingChannel: GraphqlTracingChannelFactory, - options: GraphqlDiagnosticChannelsOptions = {}, + options: GraphQLOptions = {}, ): void { const ignoreResolveSpans = options.ignoreResolveSpans !== false; const ignoreTrivialResolveSpans = options.ignoreTrivialResolveSpans !== false; diff --git a/packages/server-utils/src/integrations/graphql/index.ts b/packages/server-utils/src/integrations/graphql/index.ts index b20d6bfef451..bd204e21eda7 100644 --- a/packages/server-utils/src/integrations/graphql/index.ts +++ b/packages/server-utils/src/integrations/graphql/index.ts @@ -1,8 +1,7 @@ import * as diagnosticsChannel from 'node:diagnostics_channel'; -import type { Client, IntegrationFn } from '@sentry/core'; -import { defineIntegration, extendIntegration } from '@sentry/core'; -import { graphqlIntegration as graphqlNativeIntegration } from '../../graphql'; -import type { GraphqlDiagnosticChannelsOptions } from '../../graphql/graphql-dc-subscriber'; +import type { IntegrationFn } from '@sentry/core'; +import { defineIntegration, waitForTracingChannelBinding } from '@sentry/core'; +import { subscribeGraphqlDiagnosticChannels, type GraphQLOptions } from './graphql-dc-subscriber'; import { CHANNELS } from '../../orchestrion/channels'; import { graphqlModuleNames } from '../../orchestrion/config/graphql'; import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation'; @@ -28,7 +27,7 @@ interface GraphqlChannelContext { error?: unknown; } -function getOptionsWithDefaults(options: GraphqlDiagnosticChannelsOptions): GraphqlResolvedConfig { +function getOptionsWithDefaults(options: GraphQLOptions): GraphqlResolvedConfig { return { ignoreResolveSpans: options.ignoreResolveSpans !== false, ignoreTrivialResolveSpans: options.ignoreTrivialResolveSpans !== false, @@ -36,7 +35,7 @@ function getOptionsWithDefaults(options: GraphqlDiagnosticChannelsOptions): Grap }; } -const _graphqlIntegration = ((options: GraphqlDiagnosticChannelsOptions = {}) => { +const _graphqlIntegration = ((options: GraphQLOptions = {}) => { const config = getOptionsWithDefaults(options); const getConfig = (): GraphqlResolvedConfig => config; @@ -45,6 +44,9 @@ const _graphqlIntegration = ((options: GraphqlDiagnosticChannelsOptions = {}) => setup(client) { invokeOrchestrionInstrumentation(client, graphqlModuleNames, instrumentGraphql, [config, getConfig]); }, + setupOnce() { + setupNativeGraphQLInstrumentation(options); + }, }; }) satisfies IntegrationFn; @@ -66,30 +68,20 @@ function instrumentGraphql(config: GraphqlResolvedConfig, getConfig: () => Graph ); } -/** - * Orchestrion-driven graphql integration for graphql v14–16 (v17 publishes native - * `diagnostics_channel` events handled by `@sentry/server-utils`'s graphql integration instead). - * - * Subscribes to the `orchestrion:graphql:{parse,validate,execute}` channels the orchestrion code - * transform injects into `graphql`'s `language/parser.js`, `validation/validate.js` and - * `execution/execute.js`, emitting spans identical to the native path. Requires the orchestrion - * runtime hook or bundler plugin. - */ -export const graphqlIntegration = defineIntegration(_graphqlIntegration); +function setupNativeGraphQLInstrumentation(options: GraphQLOptions) { + if (!diagnosticsChannel.tracingChannel) { + return; + } + + // Subscribe to graphql's native tracing channels (graphql >= 17). + // This is a no-op on versions that don't publish to the channels, so it is always safe to call. + waitForTracingChannelBinding(() => { + subscribeGraphqlDiagnosticChannels(diagnosticsChannel.tracingChannel, options); + }); +} /** - * The complete graphql diagnostics-channel integration: the native subscriber (graphql v17) composed - * with the orchestrion subscriber (v14–16), so opting into injection instruments every supported - * version via diagnostics channels without the OTel patcher. Reuses the OTel `Graphql` name so - * enabling injection swaps this in for it. + * Instrument the graphql library. + * This works for graphql v14-v17. */ -export const graphqlDiagnosticsIntegration = (options?: GraphqlDiagnosticChannelsOptions) => { - const orchestrion = graphqlIntegration(options); - // The native half is the base integration's own `setupOnce`; the orchestrion half - // registers lazily via `setup` (only once `graphql` is injected), so it isn't - // merged onto the base `setupOnce` — both run. - return extendIntegration(graphqlNativeIntegration(options), { - name: INTEGRATION_NAME, - setup: (client: Client) => orchestrion.setup?.(client), - }); -}; +export const graphqlIntegration = defineIntegration(_graphqlIntegration); diff --git a/packages/server-utils/src/integrations/graphql/spans.ts b/packages/server-utils/src/integrations/graphql/spans.ts index 45426cad6509..c3025e05d3ca 100644 --- a/packages/server-utils/src/integrations/graphql/spans.ts +++ b/packages/server-utils/src/integrations/graphql/spans.ts @@ -14,13 +14,8 @@ import { SPAN_STATUS_ERROR, startInactiveSpan, } from '@sentry/core'; -import type { GraphqlDocumentNode } from '../../graphql/utils'; -import { - collectGraphqlDocument, - getOperationSpanName, - hasResultErrors, - renameRootSpanWithOperation, -} from '../../graphql/utils'; +import type { GraphqlDocumentNode } from './types'; +import { collectGraphqlDocument, getOperationSpanName, hasResultErrors, renameRootSpanWithOperation } from './utils'; import { GRAPHQL_DATA_SYMBOL, ORIGIN, SPAN_NAME_EXECUTE, SPAN_NAME_PARSE, SPAN_NAME_VALIDATE } from './constants'; import { getOperation, wrapFields, wrapFieldResolver } from './resolvers'; import type { diff --git a/packages/server-utils/src/integrations/graphql/types.ts b/packages/server-utils/src/integrations/graphql/types.ts index 992d8ef1d1ed..2c105348661f 100644 --- a/packages/server-utils/src/integrations/graphql/types.ts +++ b/packages/server-utils/src/integrations/graphql/types.ts @@ -31,3 +31,19 @@ export interface GraphqlResolvedConfig { ignoreTrivialResolveSpans: boolean; useOperationNameForRootSpan: boolean; } + +/** Minimal shape of a graphql-js lexer token, enough to locate literal spans for redaction. */ +export interface GraphqlToken { + kind: string; + start: number; + end: number; + next?: GraphqlToken | null; +} + +/** Minimal shape of a parsed graphql-js `DocumentNode`, enough to read its source and tokens. */ +export interface GraphqlDocumentNode { + loc?: { + startToken?: GraphqlToken; + source?: { body?: string }; + }; +} diff --git a/packages/server-utils/src/graphql/utils.ts b/packages/server-utils/src/integrations/graphql/utils.ts similarity index 93% rename from packages/server-utils/src/graphql/utils.ts rename to packages/server-utils/src/integrations/graphql/utils.ts index 610eaf26f9d6..c92f10d6da93 100644 --- a/packages/server-utils/src/graphql/utils.ts +++ b/packages/server-utils/src/integrations/graphql/utils.ts @@ -1,6 +1,7 @@ import { SENTRY_GRAPHQL_OPERATION } from '@sentry/conventions/attributes'; import type { Span, SpanAttributeValue } from '@sentry/core'; import { getClient, isObjectLike, getRootSpan, spanToJSON, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import type { GraphqlDocumentNode, GraphqlToken } from './types'; // Same key the OTel path uses, so renames stay consistent across both. const ORIGINAL_DESCRIPTION_ATTRIBUTE = 'original-description'; @@ -10,22 +11,6 @@ const ORIGINAL_DESCRIPTION_ATTRIBUTE = 'original-description'; // `graphql.document`. Mirrors the legacy OTel instrumentation's redaction set. const REDACTED_LITERAL_KINDS = new Set(['Int', 'Float', 'String', 'BlockString']); -/** Minimal shape of a graphql-js lexer token, enough to locate literal spans for redaction. */ -interface GraphqlToken { - kind: string; - start: number; - end: number; - next?: GraphqlToken | null; -} - -/** Minimal shape of a parsed graphql-js `DocumentNode`, enough to read its source and tokens. */ -export interface GraphqlDocumentNode { - loc?: { - startToken?: GraphqlToken; - source?: { body?: string }; - }; -} - /** * Rename the enclosing root span to include the operation name(s), e.g. `GET /graphql (query GetUser)`. */ diff --git a/packages/server-utils/src/orchestrion/index.ts b/packages/server-utils/src/orchestrion/index.ts index c9651e24af2a..fc5065eeb35d 100644 --- a/packages/server-utils/src/orchestrion/index.ts +++ b/packages/server-utils/src/orchestrion/index.ts @@ -4,7 +4,7 @@ import { awsIntegration } from '../integrations/aws-sdk'; import { dataloaderIntegration } from '../integrations/dataloader'; import { genericPoolIntegration } from '../integrations/generic-pool'; import { googleGenAIIntegration } from '../integrations/google-genai'; -import { graphqlIntegration, graphqlDiagnosticsIntegration } from '../integrations/graphql'; +import { graphqlIntegration } from '../integrations/graphql'; import { hapiIntegration } from '../integrations/hapi'; import { koaIntegration } from '../integrations/koa'; import { redisIntegration } from '../integrations/redis'; @@ -44,7 +44,6 @@ export { genericPoolIntegration, googleGenAIIntegration, graphqlIntegration, - graphqlDiagnosticsIntegration, hapiIntegration, koaIntegration, redisIntegration, @@ -108,7 +107,7 @@ export const channelIntegrations = { hapiIntegration, koaIntegration, expressIntegration, - graphqlIntegration: graphqlDiagnosticsIntegration, + graphqlIntegration, kafkajsIntegration, tediousIntegration, awsIntegration, diff --git a/packages/server-utils/test/graphql/utils.test.ts b/packages/server-utils/test/graphql/utils.test.ts index 8b7ebbd95a66..d8e7a30b946d 100644 --- a/packages/server-utils/test/graphql/utils.test.ts +++ b/packages/server-utils/test/graphql/utils.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import * as core from '@sentry/core'; -import { collectGraphqlDocument } from '../../src/graphql/utils'; -import type { GraphqlDocumentNode } from '../../src/graphql/utils'; +import { collectGraphqlDocument } from '../../src/integrations/graphql/utils'; +import type { GraphqlDocumentNode } from '../../src/integrations/graphql/types'; vi.spyOn(core, 'getClient');