fix(graphql): make the resolve span the active scope inside resolvers#8208
Conversation
Overall package sizeSelf size: 6.52 MB Dependency sizes| name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.2.0 | 104.26 kB | 843.44 kB | | opentracing | 0.14.7 | 194.81 kB | 194.81 kB | | dc-polyfill | 0.1.11 | 25.74 kB | 25.74 kB |🤖 This report was automatically generated by heaviest-objects-in-the-universe |
🎉 All green!🧪 All tests passed 🎯 Code Coverage (details) 🔗 Commit SHA: 27bff30 | Docs | Datadog PR Page | Give us feedback! |
BenchmarksBenchmark execution time: 2026-07-02 16:42:25 Comparing candidate commit 27bff30 in PR branch Found 0 performance improvements and 2 performance regressions! Performance is the same for 2255 metrics, 29 unstable metrics.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #8208 +/- ##
==========================================
- Coverage 93.69% 93.64% -0.05%
==========================================
Files 889 898 +9
Lines 50856 52364 +1508
Branches 11830 12324 +494
==========================================
+ Hits 47647 49036 +1389
- Misses 3209 3328 +119 Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
aabc7b1 to
dfa1d6f
Compare
dfa1d6f to
bda1b10
Compare
A `tracer.startSpan(...)` call inside a graphql resolver parented under
`graphql.execute` instead of the field's `graphql.resolve` span, because the
resolver ran in whatever store was active when execute invoked it — the
resolve span was created but never entered on `storage('legacy')`.
Run the resolver inside the resolve span's store via `legacyStorage.run`, which
scopes only the synchronous resolver body: a returned promise unwinds the frame,
so its continuation still runs in the parent scope. Collapsed list siblings
re-enter the first sibling's store and share the same `graphql.resolve` scope.
The store entry is confined to the span-creating path. The fast paths — depth=0
with no IAST/AppSec subscriber, no root context, and depth- or collapse-filtered
fields — keep their bare `resolve.apply` and pay nothing for the active scope.
Co-authored-by: Luciano Leggieri <[email protected]>
bda1b10 to
fa8f8d1
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fa8f8d1ded
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
With `collapse: true`, a list field's resolvers share one `graphql.resolve` span. The first sibling's synchronous resolver finishes that span via its own callback, so later siblings re-entering its store ran in an already-closed scope: a user span started inside them parented to and outlived the finished span, inverting the trace timing. Run the collapsed siblings in the parent store instead, which stays open for the lifetime of the execution.
1b27788 to
27bff30
Compare
…#8208) A `tracer.startSpan(...)` call inside a graphql resolver parented under `graphql.execute` instead of the field's `graphql.resolve` span, because the resolver ran in whatever store was active when execute invoked it — the resolve span was created but never entered on `storage('legacy')`. Run the resolver inside the resolve span's store via `legacyStorage.run`, which scopes only the synchronous resolver body: a returned promise unwinds the frame, so its continuation still runs in the parent scope. Collapsed list siblings re-enter the first sibling's store and share the same `graphql.resolve` scope. The store entry is confined to the span-creating path. The fast paths — depth=0 with no IAST/AppSec subscriber, no root context, and depth- or collapse-filtered fields — keep their bare `resolve.apply` and pay nothing for the active scope. --------- Co-authored-by: Luciano Leggieri <[email protected]>
…#8208) A `tracer.startSpan(...)` call inside a graphql resolver parented under `graphql.execute` instead of the field's `graphql.resolve` span, because the resolver ran in whatever store was active when execute invoked it — the resolve span was created but never entered on `storage('legacy')`. Run the resolver inside the resolve span's store via `legacyStorage.run`, which scopes only the synchronous resolver body: a returned promise unwinds the frame, so its continuation still runs in the parent scope. Collapsed list siblings re-enter the first sibling's store and share the same `graphql.resolve` scope. The store entry is confined to the span-creating path. The fast paths — depth=0 with no IAST/AppSec subscriber, no root context, and depth- or collapse-filtered fields — keep their bare `resolve.apply` and pay nothing for the active scope. --------- Co-authored-by: Luciano Leggieri <[email protected]>
tracer.startSpan(...)calls inside a graphql resolver parented undergraphql.executeinstead of the field'sgraphql.resolvespan, because the resolve plugin populatedfieldCtx.currentStorewithout entering it onstorage('legacy'). Migrate the resolve channel to the samerunStores+bindStartshapewrapExecutealready uses in this file: the plugin'sbindStartreturns the new store on first encounter, the parent ALS store on depth/collapse-filtered fields, and the cached store when the same field is revisited (collapsed list siblings).apm:graphql:resolve:startsubscribers (today: IAST taint-tracking) now fire once per resolver invocation rather than once per field path. For non-collapsed queries that is unchanged; for collapsed list fields it tags every sibling's args instead of only the first.Co-authored-by: Luciano Leggieri [email protected] @lukiano
Refs: #7624