Skip to content

feat(graphql): add mercurius support with a top-level graphql.request span#9142

Merged
BridgeAR merged 8 commits into
masterfrom
BridgeAR/2026-06-29-mercurius
Jul 8, 2026
Merged

feat(graphql): add mercurius support with a top-level graphql.request span#9142
BridgeAR merged 8 commits into
masterfrom
BridgeAR/2026-06-29-mercurius

Conversation

@BridgeAR

Copy link
Copy Markdown
Member

Summary

Mercurius funnels every operation through one async function and parses,
validates, and executes internally, so a query served from its JIT-compiled
path runs no graphql.execute and produced no span at all. A top-level
graphql.request span now opens at that funnel (fastifyGraphQl) via
orchestrion, parents the parse/validate/execute sub-spans, and carries the
request text plus operation name/type. It survives the JIT warm path, where no
execute span fires.

  1. The request boundary only sees the raw source and operationName; the execute
    sub-plugin backfills the precise operation signature onto the request span
    once the document is parsed, so the hot path never re-parses.
  2. v0 keeps the cross-tracer graphql.request name; v1 uses
    graphql.server.request. The v1 overlap with the execute span's name is a
    known cross-tracer wart left for a separate breaking change.
  3. The funnel yields one request span per operation, so a batched request
    produces one span per operation rather than one per HTTP request.

Test plan

New mercurius CI job runs the plugin spec across majors 13-16 (15+ gated to
Node 20+). Coverage: request-span source/operation tags, execute parented under
request with a shared resource, resolver and pre-execute validation errors,
anonymous and programmatic app.graphql() calls, the JIT warm path, batched
queries, and source on/off.

Fixes: #1141

@dd-octo-sts

dd-octo-sts Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Overall package size

Self size: 6.68 MB
Deduped: 7.34 MB
No deduping: 7.34 MB

Dependency sizes | name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.3.1 | 122.62 kB | 437.94 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

@datadog-prod-us1-6

datadog-prod-us1-6 Bot commented Jun 29, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 96.51% (+0.01%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: b6c2dbe | Docs | Datadog PR Page | Give us feedback!

@pr-commenter

pr-commenter Bot commented Jun 29, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-07-08 16:14:39

Comparing candidate commit b6c2dbe in PR branch BridgeAR/2026-06-29-mercurius with baseline commit 221c2fb in branch master.

📊 Benchmarking dashboard

Found 0 performance improvements and 0 performance regressions! Performance is the same for 2311 metrics, 47 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

Unstable benchmarks

These benchmarks have a confidence interval too wide to call a change; treat them as noise rather than signal.

scenario:appsec-appsec-enabled-24

  • unstable execution_time [-209.791ms; +202.954ms] or [-7.865%; +7.608%]

scenario:appsec-appsec-enabled-26

  • unstable execution_time [-223.613ms; +235.789ms] or [-8.776%; +9.254%]

scenario:appsec-appsec-enabled-with-attacks-24

  • unstable execution_time [-152.493ms; +160.104ms] or [-4.932%; +5.178%]

scenario:appsec-appsec-enabled-with-attacks-26

  • unstable execution_time [-200848.057µs; +201936.623µs] or [-6.805%; +6.842%]

scenario:appsec-control-20

  • unstable execution_time [-124920.281µs; +124303.481µs] or [-7.569%; +7.532%]

scenario:appsec-control-24

  • unstable execution_time [-115064.437µs; +114672.070µs] or [-9.277%; +9.246%]

scenario:appsec-control-26

  • unstable execution_time [-135.984ms; +104.558ms] or [-11.179%; +8.596%]

scenario:appsec-iast-no-vulnerability-control-20

  • unstable execution_time [-14877.506µs; +16227.040µs] or [-5.720%; +6.239%]

scenario:appsec-iast-no-vulnerability-iast-enabled-always-active-20

  • unstable execution_time [-18.827ms; +9.704ms] or [-7.252%; +3.738%]

scenario:appsec-iast-no-vulnerability-iast-enabled-default-config-20

  • unstable execution_time [-14.265ms; +17.253ms] or [-5.589%; +6.759%]

scenario:appsec-iast-with-vulnerability-iast-enabled-default-config-20

  • unstable execution_time [-27.634ms; +33.271ms] or [-4.939%; +5.947%]

scenario:debugger-line-probe-with-snapshot-default-26

  • unstable cpu_user_time [-3146.837ms; +4742.805ms] or [-29.243%; +44.074%]
  • unstable execution_time [-3189.677ms; +4787.140ms] or [-27.728%; +41.615%]
  • unstable instructions [-27.6G instructions; +41.7G instructions] or [-30.569%; +46.147%]
  • unstable max_rss_usage [-9.136MB; +15.227MB] or [-5.591%; +9.318%]
  • unstable throughput [-967.274op/s; +662.865op/s] or [-32.652%; +22.376%]

scenario:debugger-line-probe-with-snapshot-minimal-24

  • unstable cpu_user_time [-3.114s; +0.089s] or [-32.311%; +0.919%]
  • unstable execution_time [-3.171s; +0.122s] or [-30.528%; +1.174%]
  • unstable instructions [-26.5G instructions; +0.6G instructions] or [-33.555%; +0.749%]
  • unstable throughput [-62.101op/s; +835.948op/s] or [-1.916%; +25.796%]

scenario:debugger-line-probe-with-snapshot-minimal-26

  • unstable cpu_user_time [-2.112s; +4.592s] or [-21.121%; +45.913%]
  • unstable execution_time [-2.067s; +4.569s] or [-19.266%; +42.575%]
  • unstable instructions [-18.8G instructions; +40.5G instructions] or [-22.487%; +48.383%]
  • unstable max_rss_usage [-6.350MB; +14.494MB] or [-3.934%; +8.980%]
  • unstable throughput [-991.500op/s; +442.249op/s] or [-31.887%; +14.223%]

scenario:debugger-line-probe-without-snapshot-24

  • unstable cpu_user_time [-1.932s; +4.058s] or [-22.050%; +46.306%]
  • unstable execution_time [-2.028s; +4.186s] or [-21.424%; +44.211%]
  • unstable instructions [-16.2G instructions; +34.7G instructions] or [-22.572%; +48.355%]
  • unstable max_rss_usage [-7.777MB; +17.312MB] or [-4.898%; +10.903%]
  • unstable throughput [-1148.609op/s; +571.098op/s] or [-32.476%; +16.147%]

scenario:debugger-line-probe-without-snapshot-26

  • unstable cpu_user_time [-3081.003ms; +4710.322ms] or [-28.709%; +43.891%]
  • unstable execution_time [-3090.594ms; +4714.283ms] or [-27.000%; +41.185%]
  • unstable instructions [-27.9G instructions; +41.7G instructions] or [-30.895%; +46.110%]
  • unstable max_rss_usage [-9.398MB; +14.482MB] or [-5.744%; +8.852%]
  • unstable throughput [-960.882op/s; +635.204op/s] or [-32.322%; +21.367%]

scenario:dogstatsd-aggregated-26

  • unstable cpu_usage_percentage [-7.945%; +4.150%]
  • unstable execution_time [-58.847ms; +116.926ms] or [-5.766%; +11.457%]
  • unstable throughput [-1274420.559op/s; +603139.507op/s] or [-8.545%; +4.044%]

scenario:dogstatsd-with-tags-20

  • unstable cpu_user_time [-430.358ms; +268.151ms] or [-8.547%; +5.325%]
  • unstable execution_time [-425.742ms; +269.954ms] or [-8.329%; +5.281%]
  • unstable throughput [-93712.597op/s; +145447.448op/s] or [-5.707%; +8.858%]

scenario:plugin-claude-agent-sdk-compact-stream-scan-26

  • unstable cpu_usage_percentage [-8.227%; +4.221%]
  • unstable cpu_user_time [-3506.652µs; +3898.652µs] or [-5.935%; +6.599%]

scenario:plugin-graphql-long-with-depth-on-max-20

  • unstable cpu_user_time [-692.669ms; +756.953ms] or [-5.396%; +5.897%]
  • unstable execution_time [-695.308ms; +766.273ms] or [-5.303%; +5.844%]
  • unstable throughput [-3.519op/s; +3.200op/s] or [-5.732%; +5.213%]

scenario:test-optimization-large-suite-20

  • unstable max_rss_usage [-3.120MB; +6.935MB] or [-3.925%; +8.725%]

@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.55%. Comparing base (221c2fb) to head (b6c2dbe).
⚠️ Report is 8 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff            @@
##           master    #9142    +/-   ##
========================================
  Coverage   96.54%   96.55%            
========================================
  Files         915      918     +3     
  Lines      121018   121394   +376     
  Branches    20840    20990   +150     
========================================
+ Hits       116835   117207   +372     
- Misses       4183     4187     +4     
Flag Coverage Δ
aiguard 53.62% <100.00%> (-0.01%) ⬇️
aiguard-integration 57.05% <100.00%> (+0.03%) ⬆️
apm-bucket-0 53.84% <100.00%> (-0.01%) ⬇️
apm-bucket-1 58.15% <100.00%> (-0.01%) ⬇️
apm-bucket-2 56.17% <100.00%> (+<0.01%) ⬆️
apm-capabilities-tracing 60.27% <53.15%> (-0.04%) ⬇️
apm-integrations-aerospike 53.01% <100.00%> (+<0.01%) ⬆️
apm-integrations-confluentinc-kafka-javascript 57.05% <100.00%> (-0.08%) ⬇️
apm-integrations-couchbase 53.32% <100.00%> (+<0.01%) ⬆️
apm-integrations-http 58.03% <100.00%> (-0.01%) ⬇️
apm-integrations-kafkajs 57.77% <100.00%> (+<0.01%) ⬆️
apm-integrations-next 54.30% <100.00%> (+<0.01%) ⬆️
apm-integrations-prisma 54.23% <100.00%> (-0.01%) ⬇️
apm-integrations-tedious 53.10% <100.00%> (+<0.01%) ⬆️
appsec 69.11% <100.00%> (-0.03%) ⬇️
appsec-express_fastify_graphql 65.67% <65.26%> (-0.08%) ⬇️
appsec-integration 45.60% <66.30%> (+0.09%) ⬆️
appsec-kafka_ldapjs_lodash 59.16% <100.00%> (-0.01%) ⬇️
appsec-mongodb-core_mongoose_mysql 62.32% <100.00%> (-0.02%) ⬇️
appsec-next 52.90% <100.00%> (-0.01%) ⬇️
appsec-node-serialize_passport_postgres 61.95% <100.00%> (-0.02%) ⬇️
appsec-sourcing_stripe_template 60.30% <100.00%> (-0.02%) ⬇️
debugger 65.74% <100.00%> (+0.01%) ⬆️
instrumentations-bucket-0 48.80% <100.00%> (+<0.01%) ⬆️
instrumentations-bucket-1 55.02% <100.00%> (?)
instrumentations-bucket-10 56.69% <100.00%> (-0.01%) ⬇️
instrumentations-bucket-11 48.80% <100.00%> (+<0.01%) ⬆️
instrumentations-bucket-12 48.90% <100.00%> (+<0.01%) ⬆️
instrumentations-bucket-13 48.73% <100.00%> (+<0.01%) ⬆️
instrumentations-bucket-2 49.79% <100.00%> (+<0.01%) ⬆️
instrumentations-bucket-3 53.88% <100.00%> (-0.01%) ⬇️
instrumentations-bucket-4 49.22% <100.00%> (+<0.01%) ⬆️
instrumentations-bucket-5 53.06% <100.00%> (-0.01%) ⬇️
instrumentations-bucket-6 55.67% <100.00%> (-0.01%) ⬇️
instrumentations-bucket-7 53.65% <100.00%> (-0.01%) ⬇️
instrumentations-bucket-8 54.69% <100.00%> (-0.01%) ⬇️
instrumentations-bucket-9 56.15% <100.00%> (-0.01%) ⬇️
instrumentations-instrumentation-couchbase 48.23% <100.00%> (+<0.01%) ⬆️
instrumentations-integration-esbuild 33.97% <100.00%> (+0.06%) ⬆️
llmobs-ai_anthropic_bedrock 57.79% <100.00%> (-0.01%) ⬇️
llmobs-bucket-1 56.85% <100.00%> (-0.01%) ⬇️
llmobs-openai 57.62% <100.00%> (-0.01%) ⬇️
llmobs-sdk 60.03% <100.00%> (-0.01%) ⬇️
llmobs-vertex-ai 54.27% <100.00%> (-0.01%) ⬇️
master-coverage 96.55% <100.00%> (?)
openfeature 54.65% <100.00%> (+0.04%) ⬆️
openfeature-unit 49.88% <100.00%> (+<0.01%) ⬆️
platform-core_esbuild_instrumentations-misc 38.45% <100.00%> (+0.03%) ⬆️
platform-integration 62.22% <100.00%> (+0.02%) ⬆️
platform-shimmer_unit-guardrails_webpack 37.24% <100.00%> (+0.02%) ⬆️
plugins-bucket-0 53.23% <100.00%> (+<0.01%) ⬆️
plugins-bucket-1 55.12% <100.00%> (+0.04%) ⬆️
plugins-bucket-11 55.63% <100.00%> (-1.17%) ⬇️
plugins-bucket-17 ?
plugins-bucket-18 57.12% <54.52%> (-0.53%) ⬇️
plugins-bucket-19 55.44% <100.00%> (-1.75%) ⬇️
plugins-bucket-20 57.51% <66.30%> (-1.93%) ⬇️
plugins-bucket-4 53.90% <100.00%> (-0.01%) ⬇️
plugins-bullmq_cassandra_cookie 57.46% <100.00%> (+<0.01%) ⬆️
plugins-cookie-parser_crypto_dd-trace-api 52.37% <100.00%> (-0.01%) ⬇️
plugins-fetch_fs_generic-pool 54.52% <100.00%> (+<0.01%) ⬆️
plugins-google-cloud-pubsub_grpc_handlebars 59.93% <100.00%> (-0.01%) ⬇️
plugins-hapi_hono_ioredis 55.92% <100.00%> (+<0.01%) ⬆️
plugins-jest_knex_langgraph 51.72% <100.00%> (+<0.01%) ⬆️
plugins-ldapjs_light-my-request_limitd-client 53.47% <100.00%> (-0.03%) ⬇️
plugins-lodash_mariadb_memcached 54.27% <100.00%> (+<0.01%) ⬆️
plugins-moleculer_mongodb_mongodb-core 57.35% <100.00%> (?)
plugins-mongodb_mongodb-core_mongoose ?
plugins-mongoose_multer_mysql 54.77% <100.00%> (?)
plugins-multer_mysql_mysql2 ?
plugins-mysql2_nats_node-serialize 56.27% <100.00%> (?)
plugins-nats_node-serialize_opensearch ?
plugins-opensearch_passport-http_pino 55.12% <100.00%> (?)
plugins-passport-http_pino_postgres ?
plugins-postgres_process_pug 54.35% <100.00%> (?)
plugins-process_pug_redis ?
plugins-redis_router_sequelize 57.10% <100.00%> (?)
plugins-test-and-upstream-rhea_undici_url 57.08% <100.00%> (?)
plugins-undici_url_valkey ?
plugins-valkey_vm_winston 53.99% <100.00%> (?)
plugins-vm_winston_ws ?
plugins-ws 54.85% <100.00%> (?)
profiling 58.25% <100.00%> (-0.01%) ⬇️
serverless-aws-sdk-aws-sdk 50.71% <100.00%> (+<0.01%) ⬆️
serverless-aws-sdk-bedrockruntime 50.73% <100.00%> (+<0.01%) ⬆️
serverless-aws-sdk-client 52.01% <100.00%> (-0.01%) ⬇️
serverless-aws-sdk-dynamodb 51.86% <100.00%> (+0.06%) ⬆️
serverless-aws-sdk-eventbridge 46.27% <100.00%> (+<0.01%) ⬆️
serverless-aws-sdk-kinesis 54.76% <100.00%> (+<0.01%) ⬆️
serverless-aws-sdk-lambda 52.84% <100.00%> (+<0.01%) ⬆️
serverless-aws-sdk-s3 51.58% <100.00%> (+<0.01%) ⬆️
serverless-aws-sdk-serverless-peer-service 54.78% <100.00%> (-0.01%) ⬇️
serverless-aws-sdk-sns 55.53% <100.00%> (+<0.01%) ⬆️
serverless-aws-sdk-sqs 55.96% <100.00%> (-0.06%) ⬇️
serverless-aws-sdk-stepfunctions 51.30% <100.00%> (+<0.01%) ⬆️
serverless-aws-sdk-util 48.52% <100.00%> (+<0.01%) ⬆️
serverless-bucket-0 55.28% <100.00%> (+0.04%) ⬆️
serverless-bucket-1 56.07% <100.00%> (-0.01%) ⬇️
test-optimization-cucumber 73.00% <100.00%> (+0.02%) ⬆️
test-optimization-cypress 66.43% <100.00%> (+0.03%) ⬆️
test-optimization-jest 73.91% <100.00%> (-0.04%) ⬇️
test-optimization-mocha 74.33% <100.00%> (+0.05%) ⬆️
test-optimization-playwright-playwright-atr 61.63% <100.00%> (+0.04%) ⬆️
test-optimization-playwright-playwright-efd 61.83% <100.00%> (+0.04%) ⬆️
test-optimization-playwright-playwright-final-status 61.78% <100.00%> (+0.04%) ⬆️
test-optimization-playwright-playwright-impacted-tests 61.50% <100.00%> (+0.21%) ⬆️
test-optimization-playwright-playwright-reporting 61.41% <100.00%> (-0.15%) ⬇️
test-optimization-playwright-playwright-test-management 62.32% <100.00%> (-0.08%) ⬇️
test-optimization-playwright-playwright-test-span 61.55% <100.00%> (-0.01%) ⬇️
test-optimization-selenium 60.78% <100.00%> (-0.09%) ⬇️
test-optimization-testopt 59.19% <100.00%> (+0.12%) ⬆️
test-optimization-vitest 72.77% <100.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@BridgeAR
BridgeAR marked this pull request as ready for review July 1, 2026 16:24
@BridgeAR
BridgeAR requested review from a team as code owners July 1, 2026 16:25
@BridgeAR
BridgeAR requested review from tlhunter and removed request for a team July 1, 2026 16:25

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b9e20ef6d1

ℹ️ 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".

Comment thread packages/datadog-plugin-mercurius/test/integration-test/client.spec.js Outdated
Comment thread packages/datadog-plugin-graphql/src/execute.js Outdated
@BridgeAR
BridgeAR marked this pull request as draft July 2, 2026 00:47
@BridgeAR
BridgeAR force-pushed the BridgeAR/2026-06-29-mercurius branch from e9fe94c to 0c33ee0 Compare July 2, 2026 11:56
@BridgeAR
BridgeAR marked this pull request as ready for review July 7, 2026 14:19

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ecc38dc2d2

ℹ️ 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".

Comment thread packages/datadog-plugin-graphql/src/execute.js Outdated
Comment thread packages/datadog-plugin-graphql/src/execute.js Outdated
BridgeAR added 6 commits July 8, 2026 00:04
… span

Mercurius funnels every operation through one async function and parses,
validates, and executes internally, so a query served from its JIT-compiled
path runs no `graphql.execute` and produced no span at all. A top-level
`graphql.request` span now opens at that funnel (`fastifyGraphQl`) via
orchestrion, parents the parse/validate/execute sub-spans, and carries the
request text plus operation name/type. It survives the JIT warm path, where
no execute span fires.

1. The request boundary only sees the raw source and operationName; the
   execute sub-plugin backfills the precise operation signature onto the
   request span once the document is parsed, so the hot path never re-parses.
2. v0 keeps the cross-tracer `graphql.request` name; v1 uses
   `graphql.server.request`. The v1 overlap with the execute span's name is a
   known cross-tracer wart left for a separate breaking change.
3. The funnel yields one request span per operation, so a batched request
   produces one span per operation rather than one per HTTP request.

Fixes: #1141
This makes sure the top-level graphql.request span reports the operation
signature, type, and name for JIT-compiled mercurius queries. Mercurius
compiles a query after its first run and serves later requests from the
compiled path, which never calls graphql.execute. The execute sub-plugin is
where the parsed document yields the operation signature, so on that warm path
the request span was left with only the provisional operationName as its
resource and no graphql.operation.type.

The cold run now caches the computed signature/type/name keyed by the raw query
text (the same key mercurius uses for its own document LRU, bounded so a flood
of distinct queries can't grow it without limit). The request boundary reads it
back on the warm path, so it never re-parses on the hot path.
The existing plugin spec loads mercurius, fastify, and dd-trace directly
from this repo's node_modules, so it never exercises the orchestrion
rewriter's real path: an ESM app importing mercurius from an npm-installed
tarball, instrumented via the `--loader` hook. A packaging gap (a missing
`files` entry, a broken rewriter transform under real module resolution)
would pass the plugin spec but break for every actual consumer.

This spawns a sandboxed ESM app across all default/star/destructure import
shapes and the full withVersions() mercurius matrix, asserting a
graphql.request span reaches the agent for a query served through
app.graphql.
…equest span

The execute sub-plugin populated the source-keyed LRU on every parsed
document, storing the raw query text as the key even for graphql-js, Apollo,
and Yoga executions that never open a graphql.request span and can never read
the cache back. That bypassed the `source` config's privacy gate and made the
majority of GraphQL users pay an LRU set on the every-execute hot path for
entries nothing consumes.

Only a mercurius request span reads the cache on its JIT warm path, so the
write now sits behind the existing request-span presence check — graphql-js,
Apollo, and Yoga no longer touch the LRU. Moving it after the ddRequestRefined
guard also stops a re-entrant execute (Yoga's normalizedExecutor) from writing
the same entry twice.
The ESM integration spec iterated the full withVersions matrix, so the
oldest-maintenance-LTS CI leg (Node 18) sandboxed the mercurius 15+ variants.
Those ship fastify 5, which requires Node 20.9+, so the leg failed before
reaching the latest-Node run. Restrict to the 13/14 line on older Node,
mirroring the non-ESM spec's supportedOnThisNode gate.
The JIT warm-path cache was keyed by source alone. Mercurius keys its own
document LRU by source but compiles the JIT for a single operationName, and the
compiled query then serves that operation for every later request sharing the
source. When a document holds two named operations, running the first cold and
the second on the compiled path left the second request's top-level
graphql.request span carrying the first operation's resource, name, and type.

Keying the cache by source plus operationName keeps the two apart, so the warm
request reports the operation mercurius actually ran.
@BridgeAR
BridgeAR force-pushed the BridgeAR/2026-06-29-mercurius branch from bb69c4a to a907fca Compare July 7, 2026 23:17
The top-level graphql.request span was labeled only from graphql.execute, but
mercurius has two paths where execute never fires for the selected operation:

1. A multi-operation document parses once. If one operation runs cold and a
   sibling is then served exclusively through mercurius's JIT-compiled path,
   execute never runs for the sibling, so its warm request spans fell back to a
   bare operation name with no type or signature. The cold parse now caches
   every named operation in the document, so a JIT-only selection is labeled.
2. When mercurius rejects after parse/validate but before execute (unknown
   field, GET mutation), the execute-only labeling never happened. A named query
   sent without a separate operationName argument then finished its error span
   with an empty resource and no operation tags.

The refinement moves to the validate boundary, which is the first place the
parsed document exists and precedes both execute and every pre-execute
rejection. validate reads the request span and the requested operation name off
the active store the request boundary entered, since its own bindStore ctx is
not populated yet.
@BridgeAR
BridgeAR force-pushed the BridgeAR/2026-06-29-mercurius branch from a907fca to 61631e8 Compare July 7, 2026 23:33

@crysmags crysmags left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: pre-parsed DocumentNode requests lose their operation metadata on the Mercurius JIT warm path.

request.js turns non-string sources into docSource = undefined, and utils.js returns before caching when docSource === undefined. That means the cold parsed-AST call can be refined by validate, but the second call can hit Mercurius's JIT path where graphql.execute does not fire. The graphql.request span then has no cached operation metadata to recover.

Verified with:

unset OTEL_TRACES_EXPORTER OTEL_LOGS_EXPORTER OTEL_METRICS_EXPORTER
NODE_PATH=/Users/crystal.magloire/Node_Tracer/dd-trace-js/node_modules RANGE=16 \
  /Users/crystal.magloire/Node_Tracer/dd-trace-js/node_modules/.bin/mocha \
  packages/datadog-plugin-mercurius/test/index.spec.js \
  --grep "carries the operation signature for a pre-parsed document on the JIT warm path"

The focused test shape I used warms the same parsed document once, then asserts the second call:

it('carries the operation signature for a pre-parsed document on the JIT warm path', async () => {
  const query = 'query ParsedAstWarm { hello }'
  const document = require('../../../versions/graphql').get().parse(query)

  const warmup = agent.assertSomeTraces(traces => {
    const execute = traces[0].find(span => span.name === 'graphql.execute')
    assert.ok(execute)
  }, { spanResourceMatch: /ParsedAstWarm/ })

  await Promise.all([warmup, app.graphql(document)])

  const assertion = agent.assertSomeTraces(traces => {
    const request = traces[0].find(span => span.name === expectedSchema.server.opName)
    const execute = traces[0].find(span => span.name === 'graphql.execute')
    assert.ok(request)
    assert.strictEqual(execute, undefined)
    assertObjectContains(request, {
      meta: {
        'graphql.operation.type': 'query',
        'graphql.operation.name': 'ParsedAstWarm'
      }
    })
    assert.match(request.resource, /ParsedAstWarm/)
  })

  return Promise.all([assertion, app.graphql(document)])
})

Failure result: the warm request span is resource: "graphql.request" and does not include graphql.operation.name or graphql.operation.type. We should either cache parsed-AST metadata for the warm path too, or make the request boundary able to recover it some other way, and keep a focused regression test for this case.

A pre-parsed document AST reaches fastifyGraphQl as a non-string source, so
the request boundary had no query text to key the warm-path cache by and
validate returned before caching. The cold call still refined the live
request span, but mercurius keys its own document LRU by the source object's
identity and serves later requests for it from the JIT-compiled path, where
neither validate nor execute fires. Those warm request spans finished with a
bare resource and no operation tags.

The cache now keys a pre-parsed source by document identity in a WeakMap the
request boundary reads back on the warm path — mercurius hands the boundary
the same document object it keys its own LRU by, and the WeakMap releases with
the caller's document without mutating it. validate is handed the raw source
the boundary saw, since it only sees mercurius's structuredClone and the two
are different objects for a pre-parsed AST.

A source that is neither query text nor a document AST (a number reaches
validate as a truthy non-document) has no usable key; caching now skips it
rather than keying a WeakMap by a primitive, which threw a TypeError inside
the validate boundary and replaced mercurius's own rejection.

Fixes: #1141
@crysmags
crysmags dismissed their stale review July 8, 2026 19:35

addressed!

@BridgeAR
BridgeAR merged commit 0041766 into master Jul 8, 2026
655 checks passed
@BridgeAR
BridgeAR deleted the BridgeAR/2026-06-29-mercurius branch July 8, 2026 19:53
This was referenced Jul 9, 2026
juan-fernandez pushed a commit that referenced this pull request Jul 10, 2026
… span (#9142)

feat(graphql): add mercurius request span support

Mercurius can serve operations from its JIT path without calling graphql.execute, which left those requests with no GraphQL span. This opens a top-level request span at the Mercurius request boundary and refines it from the parsed document before execute-only paths can be skipped.

The request span recovers operation metadata for JIT-warm queries, multi-operation documents, validation failures before execute, and pre-parsed DocumentNode sources without reparsing on the hot path.

Fixes: #1141
juan-fernandez pushed a commit that referenced this pull request Jul 10, 2026
… span (#9142)

feat(graphql): add mercurius request span support

Mercurius can serve operations from its JIT path without calling graphql.execute, which left those requests with no GraphQL span. This opens a top-level request span at the Mercurius request boundary and refines it from the parsed document before execute-only paths can be skipped.

The request span recovers operation metadata for JIT-warm queries, multi-operation documents, validation failures before execute, and pre-parsed DocumentNode sources without reparsing on the hot path.

Fixes: #1141
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.

mercurius (a.k.a. fastify-gql) plugin support

3 participants