Skip to content

fix(graphql): rewrap late-added resolvers#3447

Open
Farhan-Abbas wants to merge 2 commits intoopen-telemetry:mainfrom
Farhan-Abbas:graphql_missing_spans_bug
Open

fix(graphql): rewrap late-added resolvers#3447
Farhan-Abbas wants to merge 2 commits intoopen-telemetry:mainfrom
Farhan-Abbas:graphql_missing_spans_bug

Conversation

@Farhan-Abbas
Copy link
Copy Markdown
Contributor

GraphQL Instrumentation: Fix Missing Spans for Late-Added Resolvers

Which problem is this PR solving?

Fixes #3362. Custom resolvers on GraphQL Query and Mutation types are not being instrumented when using Apollo Server 5 with NestJS. This causes missing graphql.resolve spans in distributed traces, making it impossible to see the performance of custom resolver logic.

Example: When executing a query like:

query getDepartment {
  department(id: "d009") {
    id
    name
    employees { items { id lastName firstName } }
  }
}

Expected spans for graphql.resolve department and graphql.resolve department.employees were completely missing—only scalar fields (id, name) generated spans.

Root cause: Apollo Server wraps resolvers after schema creation but before query execution. The old implementation used a permanent type-level "already patched" flag that skipped re-checking types, so late-added resolvers were never wrapped.

Short description of the changes

  1. Per-query visited set instead of permanent type marking

    • Replace type-level OTEL_PATCHED_SYMBOL check with a temporary visitedInThisTraversal Set
    • Each query execution starts fresh, allowing detection of new resolvers Apollo may have added
    • Prevents infinite loops on circular type references (e.g., User → friends → User)
  2. Per-resolver wrapping instead of per-type wrapping

    • Check if individual resolver functions are wrapped, not just the type
    • Allows newly attached resolvers to be wrapped on subsequent executes
    • Prevents double-wrapping with resolver-level idempotency check
  3. Changes made

    • src/instrumentation.ts: Create and pass shared visited Set to wrapFields() calls
    • src/utils.ts: Update wrapFields() to use per-traversal Set; update wrapFieldResolver() to check resolver functions directly
    • src/utils.ts (wrapFieldResolver guard): check fieldResolver[OTEL_PATCHED_SYMBOL] (incoming resolver) instead of wrappedFieldResolver[OTEL_PATCHED_SYMBOL] (new wrapper being created)
    • src/utils.ts (wrapFieldResolver guard): keep typeof fieldResolver !== 'function' as the first condition so non-function/undefined resolvers return early before any symbol lookup
  4. Verification

    • Previously missing resolver spans (for example graphql.resolve department and graphql.resolve department.employees) are now visible in traces
    • All existing package tests pass

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[instrumentation-graphql] Custom resolvers (@Query, @ResolveField) not generating spans with Apollo Server 5 + NestJS

2 participants