Skip to content

fix(express): restore loopback middleware phase ordering#9062

Merged
BridgeAR merged 2 commits into
masterfrom
BridgeAR/2026-06-25-express-loopback-test-discovery
Jun 25, 2026
Merged

fix(express): restore loopback middleware phase ordering#9062
BridgeAR merged 2 commits into
masterfrom
BridgeAR/2026-06-25-express-loopback-test-discovery

Conversation

@BridgeAR

@BridgeAR BridgeAR commented Jun 25, 2026

Copy link
Copy Markdown
Member

Summary

The loopback integration test suite was silently skipped because the
withVersions call passed the plugin export instead of the 'express'
integration name, so no installed version ever matched. Enabling it surfaced a
real regression: dd-trace replaces each express layer.handle with a tracing
wrapper, which hides the original handler from loopback's _findLayerByHandler.
That lookup is how loopback tags a layer with its phase, so the tag was never
set and app.middleware('final', ...) handlers ran in insertion order ahead of
the route handlers instead of last.

  1. fix(express): restore the _datadog_orig back-reference on the wrapped
    layer handle (dropped in the router instrumentation rewrite) so loopback can
    map the layer back to the user handler and sort by phase again.
  2. test(express): switch the suite to the 'express' name so it runs, and
    allowlist legacy loopback's deprecated util._extend load-time warning that
    the harness deprecation guard would otherwise throw on.

Note

I am working on a better fix, while that turned out to be quite a massive amount
of work, so I am suggesting we just land this as stop gap.

Refs: #643

BridgeAR added 2 commits June 25, 2026 19:59
This makes sure loopback's phase-based middleware sorting keeps working under
tracing. loopback's `_findLayerByHandler` maps an express layer back to the
user handler by scanning the layer handle's enumerable properties, then tags
the layer with its phase. Replacing `layer.handle` with the tracing wrapper hid
the original handler, so the lookup failed, the layer kept no phase, and an
`app.middleware('final', ...)` handler ran in insertion order ahead of the
route handlers instead of last. Restoring the `_datadog_orig` back-reference
that the router instrumentation rewrite dropped lets the lookup succeed again.

Refs: #643
1. The block passed the plugin export to `withVersions` instead of the
   `'express'` integration name, so no installed version matched and the suite
   never ran. Switching to the name runs it.
2. Legacy loopback emits the deprecated `util._extend` warning at module load,
   which the harness deprecation guard turns into a thrown error; allowlist that
   one message so the modules load.
@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.40%. Comparing base (4638d8e) to head (f12fcdf).
⚠️ Report is 5 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #9062      +/-   ##
==========================================
+ Coverage   93.33%   93.40%   +0.06%     
==========================================
  Files         881      885       +4     
  Lines       50636    50760     +124     
  Branches    10009    10012       +3     
==========================================
+ Hits        47259    47410     +151     
+ Misses       3377     3350      -27     
Flag Coverage Δ
aiguard-integration-active 41.73% <100.00%> (+0.01%) ⬆️
aiguard-integration-latest 41.74% <100.00%> (+<0.01%) ⬆️
aiguard-integration-maintenance 41.82% <100.00%> (+<0.01%) ⬆️
aiguard-macos 34.56% <ø> (-0.08%) ⬇️
aiguard-ubuntu 34.71% <ø> (-0.08%) ⬇️
aiguard-windows 34.39% <ø> (-0.08%) ⬇️
apm-capabilities-tracing-macos 48.22% <0.00%> (-0.01%) ⬇️
apm-capabilities-tracing-ubuntu-active 48.40% <0.00%> (+0.18%) ⬆️
apm-capabilities-tracing-ubuntu-latest 48.40% <0.00%> (?)
apm-capabilities-tracing-ubuntu-maintenance 48.44% <0.00%> (+0.17%) ⬆️
apm-capabilities-tracing-ubuntu-oldest 48.45% <0.00%> (+<0.01%) ⬆️
apm-capabilities-tracing-windows 48.22% <0.00%> (?)
apm-integrations-aerospike-18-gte.5.2.0 33.09% <ø> (-0.08%) ⬇️
apm-integrations-aerospike-20-gte.5.5.0 33.11% <ø> (-0.08%) ⬇️
apm-integrations-aerospike-22-gte.5.12.1 33.11% <ø> (-0.08%) ⬇️
apm-integrations-aerospike-22-gte.6.0.0 33.11% <ø> (-0.08%) ⬇️
apm-integrations-aerospike-eol- 33.01% <ø> (-0.08%) ⬇️
apm-integrations-child-process 34.02% <ø> (-0.08%) ⬇️
apm-integrations-confluentinc-kafka-javascript-18 ?
apm-integrations-confluentinc-kafka-javascript-20 40.04% <ø> (?)
apm-integrations-confluentinc-kafka-javascript-22 40.05% <ø> (-0.08%) ⬇️
apm-integrations-confluentinc-kafka-javascript-24 39.94% <ø> (-0.08%) ⬇️
apm-integrations-couchbase-18 33.38% <ø> (+0.03%) ⬆️
apm-integrations-couchbase-eol 33.15% <ø> (-0.08%) ⬇️
apm-integrations-dns 32.95% <ø> (-0.08%) ⬇️
apm-integrations-elasticsearch 34.14% <ø> (-0.07%) ⬇️
apm-integrations-http-latest 41.95% <0.00%> (-0.08%) ⬇️
apm-integrations-http-maintenance 42.04% <0.00%> (-0.08%) ⬇️
apm-integrations-http-oldest 41.97% <0.00%> (-0.08%) ⬇️
apm-integrations-http2 39.06% <ø> (-0.08%) ⬇️
apm-integrations-kafkajs-latest 40.08% <ø> (-0.17%) ⬇️
apm-integrations-kafkajs-oldest 40.26% <ø> (+0.02%) ⬆️
apm-integrations-net 33.64% <ø> (-0.08%) ⬇️
apm-integrations-next-11.1.4 36.30% <ø> (ø)
apm-integrations-next-12.3.7 36.30% <ø> (ø)
apm-integrations-next-13.0.0 29.10% <ø> (-0.12%) ⬇️
apm-integrations-next-13.2.0 29.10% <ø> (-0.08%) ⬇️
apm-integrations-next-13.5.11 29.23% <ø> (-0.08%) ⬇️
apm-integrations-next-14.0.0 29.20% <ø> (-0.04%) ⬇️
apm-integrations-next-14.2.35 29.16% <ø> (-0.08%) ⬇️
apm-integrations-next-14.2.6 29.16% <ø> (-0.08%) ⬇️
apm-integrations-next-14.2.7 29.16% <ø> (-0.08%) ⬇️
apm-integrations-next-15.0.0 29.20% <ø> (-0.04%) ⬇️
apm-integrations-next-15.4.0 29.23% <ø> (-0.08%) ⬇️
apm-integrations-next-latest 29.27% <ø> (-0.08%) ⬇️
apm-integrations-oracledb 33.97% <ø> (-0.08%) ⬇️
apm-integrations-prisma-18-gte.6.16.0.and.lt.7.0.0 34.79% <ø> (-0.08%) ⬇️
apm-integrations-prisma-latest-all 34.22% <ø> (?)
apm-integrations-restify 35.62% <ø> (-0.08%) ⬇️
apm-integrations-sharedb 32.55% <ø> (-0.08%) ⬇️
apm-integrations-tedious 33.89% <ø> (-0.08%) ⬇️
appsec-express 51.30% <100.00%> (-0.03%) ⬇️
appsec-fastify 48.07% <ø> (-0.08%) ⬇️
appsec-graphql 47.76% <100.00%> (-0.05%) ⬇️
appsec-integration-active 36.10% <100.00%> (+<0.01%) ⬆️
appsec-integration-latest 36.10% <100.00%> (+<0.01%) ⬆️
appsec-integration-maintenance 36.17% <100.00%> (+0.01%) ⬆️
appsec-integration-oldest 36.16% <100.00%> (+<0.01%) ⬆️
appsec-kafka 40.27% <ø> (-0.12%) ⬇️
appsec-ldapjs 39.80% <ø> (-0.07%) ⬇️
appsec-lodash 39.72% <ø> (-0.07%) ⬇️
appsec-macos 57.16% <ø> (-0.07%) ⬇️
appsec-mongodb-core 43.89% <100.00%> (-0.06%) ⬇️
appsec-mongoose 44.75% <100.00%> (-0.05%) ⬇️
appsec-mysql 47.09% <100.00%> (-0.06%) ⬇️
appsec-next-latest-11.1.4 27.39% <ø> (-0.07%) ⬇️
appsec-next-latest-12.3.7 27.40% <ø> (ø)
appsec-next-latest-13.0.0 29.18% <ø> (-0.08%) ⬇️
appsec-next-latest-13.2.0 29.20% <ø> (-0.08%) ⬇️
appsec-next-latest-13.5.11 29.29% <ø> (-0.08%) ⬇️
appsec-next-latest-14.0.0 29.22% <ø> (-0.08%) ⬇️
appsec-next-latest-14.2.35 29.22% <ø> (-0.08%) ⬇️
appsec-next-latest-14.2.6 29.22% <ø> (-0.08%) ⬇️
appsec-next-latest-14.2.7 29.22% <ø> (-0.08%) ⬇️
appsec-next-latest-15.0.0 29.22% <ø> (?)
appsec-next-latest-latest 29.26% <ø> (-0.08%) ⬇️
appsec-next-oldest-11.1.4 27.45% <ø> (-0.07%) ⬇️
appsec-next-oldest-12.3.7 29.24% <ø> (-0.08%) ⬇️
appsec-next-oldest-13.0.0 29.24% <ø> (-0.08%) ⬇️
appsec-next-oldest-13.2.0 29.50% <ø> (-0.08%) ⬇️
appsec-next-oldest-13.5.11 29.59% <ø> (-0.08%) ⬇️
appsec-next-oldest-14.0.0 29.53% <ø> (-0.08%) ⬇️
appsec-next-oldest-14.2.35 29.53% <ø> (-0.08%) ⬇️
appsec-next-oldest-14.2.6 29.53% <ø> (-0.08%) ⬇️
appsec-next-oldest-14.2.7 29.53% <ø> (-0.08%) ⬇️
appsec-next-oldest-15.0.0 29.53% <ø> (-0.08%) ⬇️
appsec-next-oldest-latest 27.60% <ø> (ø)
appsec-node-serialize 39.01% <ø> (-0.07%) ⬇️
appsec-passport 42.64% <100.00%> (-0.06%) ⬇️
appsec-postgres 46.90% <100.00%> (+0.07%) ⬆️
appsec-sourcing 38.49% <ø> (-0.07%) ⬇️
appsec-stripe 40.54% <100.00%> (-0.06%) ⬇️
appsec-template 39.30% <ø> (-0.07%) ⬇️
appsec-ubuntu 57.27% <ø> (-0.06%) ⬇️
appsec-windows 57.03% <ø> (-0.05%) ⬇️
debugger-ubuntu-active 43.35% <ø> (ø)
debugger-ubuntu-latest 43.35% <ø> (ø)
debugger-ubuntu-maintenance 43.47% <ø> (-0.29%) ⬇️
debugger-ubuntu-oldest 43.77% <ø> (ø)
instrumentations-instrumentation-ai 45.53% <ø> (ø)
instrumentations-instrumentation-aws-sdk 45.11% <ø> (ø)
instrumentations-instrumentation-bluebird 27.61% <ø> (?)
instrumentations-instrumentation-body-parser 35.65% <100.00%> (-0.07%) ⬇️
instrumentations-instrumentation-child_process 33.32% <ø> (-0.08%) ⬇️
instrumentations-instrumentation-connect 48.51% <ø> (ø)
instrumentations-instrumentation-cookie-parser 29.52% <100.00%> (-0.07%) ⬇️
instrumentations-instrumentation-couchbase-18 46.23% <ø> (ø)
instrumentations-instrumentation-couchbase-eol 46.23% <ø> (ø)
instrumentations-instrumentation-crypto 27.58% <ø> (?)
instrumentations-instrumentation-express 29.72% <100.00%> (-0.07%) ⬇️
instrumentations-instrumentation-express-mongo-sanitize 29.63% <100.00%> (-0.07%) ⬇️
instrumentations-instrumentation-express-multi-version 41.80% <ø> (ø)
instrumentations-instrumentation-express-session 35.47% <100.00%> (?)
instrumentations-instrumentation-fastify 48.47% <ø> (?)
instrumentations-instrumentation-fetch 45.21% <ø> (?)
instrumentations-instrumentation-fs 27.29% <ø> (-0.08%) ⬇️
instrumentations-instrumentation-generic-pool 27.48% <0.00%> (?)
instrumentations-instrumentation-hono 28.82% <ø> (-0.08%) ⬇️
instrumentations-instrumentation-http 37.91% <ø> (-0.08%) ⬇️
instrumentations-instrumentation-http-client-options 37.52% <ø> (-0.08%) ⬇️
instrumentations-instrumentation-kafkajs 49.19% <ø> (ø)
instrumentations-instrumentation-knex 27.60% <ø> (-0.08%) ⬇️
instrumentations-instrumentation-koa 46.11% <ø> (?)
instrumentations-instrumentation-light-my-request 35.29% <ø> (-0.08%) ⬇️
instrumentations-instrumentation-mongoose 28.71% <ø> (-0.08%) ⬇️
instrumentations-instrumentation-multer 35.33% <100.00%> (?)
instrumentations-instrumentation-mysql2 33.43% <ø> (-0.08%) ⬇️
instrumentations-instrumentation-openai-lifecycle 46.21% <ø> (?)
instrumentations-instrumentation-otel-sdk-trace 25.46% <ø> (?)
instrumentations-instrumentation-passport 39.26% <100.00%> (?)
instrumentations-instrumentation-passport-http 38.96% <100.00%> (-0.06%) ⬇️
instrumentations-instrumentation-passport-local 39.41% <100.00%> (-0.06%) ⬇️
instrumentations-instrumentation-pg 33.16% <ø> (-0.08%) ⬇️
instrumentations-instrumentation-promise 27.56% <ø> (-0.08%) ⬇️
instrumentations-instrumentation-promise-js 27.56% <ø> (?)
instrumentations-instrumentation-q 27.59% <ø> (-0.08%) ⬇️
instrumentations-instrumentation-restify 47.75% <ø> (ø)
instrumentations-instrumentation-router 43.70% <100.00%> (+0.06%) ⬆️
instrumentations-instrumentation-stripe 28.10% <ø> (-0.08%) ⬇️
instrumentations-instrumentation-url 27.41% <ø> (-0.08%) ⬇️
instrumentations-instrumentation-when 27.57% <ø> (-0.08%) ⬇️
instrumentations-instrumentation-zlib 27.46% <ø> (?)
instrumentations-integration-esbuild-0.16.12-active 24.68% <0.00%> (-0.01%) ⬇️
instrumentations-integration-esbuild-0.16.12-latest 24.68% <0.00%> (-0.01%) ⬇️
instrumentations-integration-esbuild-0.16.12-maintenance ?
instrumentations-integration-esbuild-0.16.12-oldest ?
instrumentations-integration-esbuild-latest-active 24.68% <0.00%> (-0.01%) ⬇️
instrumentations-integration-esbuild-latest-latest 24.68% <0.00%> (-0.01%) ⬇️
instrumentations-integration-esbuild-latest-maintenance 18.73% <0.00%> (-0.01%) ⬇️
instrumentations-integration-esbuild-latest-oldest 18.72% <0.00%> (-0.01%) ⬇️
llmobs-ai 35.10% <ø> (-0.07%) ⬇️
llmobs-anthropic 36.52% <ø> (-0.07%) ⬇️
llmobs-bedrock 36.01% <ø> (-0.07%) ⬇️
llmobs-google-genai 35.95% <ø> (-0.07%) ⬇️
llmobs-langchain 35.46% <ø> (-0.06%) ⬇️
llmobs-openai-latest ?
llmobs-openai-oldest 39.50% <ø> (-0.07%) ⬇️
llmobs-sdk-active 43.36% <ø> (-0.08%) ⬇️
llmobs-sdk-latest 43.36% <ø> (-0.08%) ⬇️
llmobs-sdk-maintenance 43.47% <ø> (-0.08%) ⬇️
llmobs-sdk-oldest 43.45% <ø> (-0.08%) ⬇️
llmobs-vertex-ai 35.63% <ø> (-0.07%) ⬇️
master-coverage 93.40% <100.00%> (?)
openfeature-macos 37.49% <100.00%> (+<0.01%) ⬆️
openfeature-ubuntu 37.62% <100.00%> (+<0.01%) ⬆️
openfeature-unit-active 50.00% <ø> (ø)
openfeature-unit-latest 50.00% <ø> (ø)
openfeature-unit-maintenance 50.36% <ø> (ø)
openfeature-unit-oldest 50.36% <ø> (ø)
openfeature-windows ?
platform-core 46.47% <ø> (ø)
platform-esbuild 18.66% <0.00%> (-0.01%) ⬇️
platform-instrumentations-misc 29.57% <0.00%> (-0.01%) ⬇️
platform-integration-active ?
platform-integration-latest 47.27% <100.00%> (+<0.01%) ⬆️
platform-integration-maintenance ?
platform-integration-oldest 47.32% <100.00%> (+<0.01%) ⬆️
platform-shimmer 47.50% <ø> (ø)
platform-unit-guardrails 44.42% <ø> (ø)
platform-webpack 18.31% <0.00%> (?)
plugins-aws-durable-execution-sdk-js 32.82% <ø> (-0.07%) ⬇️
plugins-axios 35.46% <ø> (ø)
plugins-azure-cosmos 35.79% <ø> (-0.24%) ⬇️
plugins-azure-event-hubs 34.82% <ø> (?)
plugins-azure-service-bus 35.33% <ø> (ø)
plugins-body-parser 36.43% <100.00%> (?)
plugins-bullmq 39.43% <ø> (-0.08%) ⬇️
plugins-cassandra 33.74% <ø> (+0.08%) ⬆️
plugins-cookie 40.72% <ø> (ø)
plugins-cookie-parser 40.55% <ø> (ø)
plugins-crypto 42.71% <ø> (ø)
plugins-dd-trace-api 33.17% <ø> (-0.08%) ⬇️
plugins-express-mongo-sanitize 40.60% <ø> (ø)
plugins-express-session 40.46% <ø> (ø)
plugins-fastify 37.76% <ø> (-0.07%) ⬇️
plugins-fetch 34.52% <0.00%> (-0.08%) ⬇️
plugins-fs 33.60% <ø> (-0.08%) ⬇️
plugins-generic-pool 40.02% <ø> (ø)
plugins-google-cloud-pubsub 41.32% <ø> (-0.07%) ⬇️
plugins-grpc 36.47% <ø> (-0.07%) ⬇️
plugins-handlebars 40.60% <ø> (ø)
plugins-hapi 35.76% <ø> (-0.08%) ⬇️
plugins-hono 35.87% <ø> (-0.08%) ⬇️
plugins-ioredis 34.23% <ø> (-0.08%) ⬇️
plugins-jest 27.03% <ø> (ø)
plugins-knex 40.06% <ø> (ø)
plugins-langgraph 32.30% <ø> (-0.08%) ⬇️
plugins-ldapjs 39.02% <ø> (ø)
plugins-light-my-request 40.16% <ø> (ø)
plugins-limitd-client 27.97% <ø> (-0.08%) ⬇️
plugins-lodash 40.21% <ø> (ø)
plugins-mariadb 35.07% <ø> (-0.08%) ⬇️
plugins-memcached 33.62% <ø> (-0.08%) ⬇️
plugins-microgateway-core 34.80% <ø> (+0.01%) ⬆️
plugins-modelcontextprotocol-sdk 32.26% <ø> (-0.08%) ⬇️
plugins-moleculer 36.53% <ø> (-0.08%) ⬇️
plugins-mongodb 35.83% <ø> (+0.01%) ⬆️
plugins-mongodb-core 35.36% <ø> (-0.08%) ⬇️
plugins-mongoose 34.51% <ø> (-0.08%) ⬇️
plugins-multer 40.52% <ø> (ø)
plugins-mysql 34.56% <ø> (-0.07%) ⬇️
plugins-mysql2 34.89% <ø> (-0.08%) ⬇️
plugins-nats 36.25% <ø> (-0.08%) ⬇️
plugins-node-serialize 40.44% <ø> (ø)
plugins-opensearch 33.74% <ø> (+0.14%) ⬆️
plugins-passport-http 40.33% <ø> (ø)
plugins-pino 29.82% <ø> (-0.08%) ⬇️
plugins-postgres 34.60% <ø> (-0.08%) ⬇️
plugins-process 42.71% <ø> (ø)
plugins-pug 40.72% <ø> (ø)
plugins-redis 34.26% <ø> (-0.07%) ⬇️
plugins-router 38.21% <100.00%> (-0.01%) ⬇️
plugins-sequelize 39.98% <ø> (ø)
plugins-test-and-upstream-amqp10 33.77% <ø> (-0.08%) ⬇️
plugins-test-and-upstream-amqplib 39.09% <ø> (-0.09%) ⬇️
plugins-test-and-upstream-apollo 34.78% <100.00%> (-0.06%) ⬇️
plugins-test-and-upstream-avsc 33.66% <ø> (-0.08%) ⬇️
plugins-test-and-upstream-bunyan 29.04% <ø> (-0.08%) ⬇️
plugins-test-and-upstream-connect 36.26% <ø> (-0.08%) ⬇️
plugins-test-and-upstream-graphql 35.92% <ø> (-0.08%) ⬇️
plugins-test-and-upstream-koa 35.97% <ø> (+0.08%) ⬆️
plugins-test-and-upstream-protobufjs 33.89% <ø> (-0.08%) ⬇️
plugins-test-and-upstream-rhea 39.25% <ø> (+0.14%) ⬆️
plugins-undici 34.95% <0.00%> (-0.08%) ⬇️
plugins-url 42.71% <ø> (ø)
plugins-valkey 33.72% <ø> (+0.06%) ⬆️
plugins-vm 42.71% <ø> (ø)
plugins-winston 29.92% <ø> (+0.14%) ⬆️
plugins-ws 37.01% <ø> (-0.08%) ⬇️
profiling-macos 43.01% <0.00%> (-0.02%) ⬇️
profiling-ubuntu 43.39% <0.00%> (?)
profiling-windows 40.86% <0.00%> (-0.07%) ⬇️
serverless-aws-sdk-latest-aws-sdk 33.06% <ø> (-0.08%) ⬇️
serverless-aws-sdk-latest-bedrockruntime 31.91% <ø> (-0.07%) ⬇️
serverless-aws-sdk-latest-client 36.33% <ø> (ø)
serverless-aws-sdk-latest-dynamodb ?
serverless-aws-sdk-latest-eventbridge 27.09% <0.00%> (-0.07%) ⬇️
serverless-aws-sdk-latest-kinesis 37.09% <ø> (-0.07%) ⬇️
serverless-aws-sdk-latest-lambda 34.33% <ø> (-0.07%) ⬇️
serverless-aws-sdk-latest-s3 32.29% <ø> (-0.07%) ⬇️
serverless-aws-sdk-latest-serverless-peer-service 39.25% <ø> (-0.08%) ⬇️
serverless-aws-sdk-latest-sns 38.09% <ø> (-0.07%) ⬇️
serverless-aws-sdk-latest-sqs 37.71% <ø> (-0.07%) ⬇️
serverless-aws-sdk-latest-stepfunctions 32.93% <ø> (-0.07%) ⬇️
serverless-aws-sdk-latest-util 46.87% <ø> (ø)
serverless-aws-sdk-oldest-aws-sdk 33.18% <ø> (-0.07%) ⬇️
serverless-aws-sdk-oldest-bedrockruntime 32.00% <ø> (-0.07%) ⬇️
serverless-aws-sdk-oldest-client 36.90% <ø> (ø)
serverless-aws-sdk-oldest-dynamodb ?
serverless-aws-sdk-oldest-eventbridge 27.18% <0.00%> (-0.07%) ⬇️
serverless-aws-sdk-oldest-kinesis 37.26% <ø> (-0.07%) ⬇️
serverless-aws-sdk-oldest-lambda 34.43% <ø> (-0.07%) ⬇️
serverless-aws-sdk-oldest-s3 32.42% <ø> (-0.07%) ⬇️
serverless-aws-sdk-oldest-serverless-peer-service 39.34% <ø> (-0.08%) ⬇️
serverless-aws-sdk-oldest-sns 38.19% <ø> (-0.07%) ⬇️
serverless-aws-sdk-oldest-sqs 37.79% <ø> (-0.07%) ⬇️
serverless-aws-sdk-oldest-stepfunctions 33.03% <ø> (?)
serverless-aws-sdk-oldest-util 47.64% <ø> (ø)
serverless-azure-durable-functions 36.94% <ø> (ø)
serverless-azure-functions-eventhubs 38.37% <ø> (ø)
serverless-azure-functions-servicebus 38.42% <ø> (?)
serverless-lambda 34.32% <ø> (-0.09%) ⬇️
test-optimization-cucumber-latest-7.0.0 49.79% <ø> (+0.11%) ⬆️
test-optimization-cucumber-latest-latest 52.49% <ø> (+0.18%) ⬆️
test-optimization-cucumber-oldest-7.0.0 49.87% <ø> (?)
test-optimization-cypress-latest-12.0.0-commonJS 47.37% <ø> (+0.05%) ⬆️
test-optimization-cypress-latest-12.0.0-esm 49.13% <ø> (+0.07%) ⬆️
test-optimization-cypress-latest-14.5.4-commonJS 48.33% <ø> (+0.07%) ⬆️
test-optimization-cypress-latest-14.5.4-esm 48.98% <ø> (+2.00%) ⬆️
test-optimization-cypress-latest-latest-commonJS 49.40% <ø> (+0.98%) ⬆️
test-optimization-cypress-latest-latest-esm 48.14% <ø> (-1.25%) ⬇️
test-optimization-cypress-oldest-12.0.0-commonJS 49.18% <ø> (+1.47%) ⬆️
test-optimization-cypress-oldest-12.0.0-esm 49.21% <ø> (+1.79%) ⬆️
test-optimization-cypress-oldest-14.5.4-commonJS 49.03% <ø> (+0.07%) ⬆️
test-optimization-cypress-oldest-14.5.4-esm 47.61% <ø> (-1.39%) ⬇️
test-optimization-jest-latest-latest 55.19% <ø> (+0.07%) ⬆️
test-optimization-jest-latest-oldest 54.13% <ø> (+0.07%) ⬆️
test-optimization-jest-oldest-latest 49.79% <ø> (-5.37%) ⬇️
test-optimization-jest-oldest-oldest 54.14% <ø> (+5.37%) ⬆️
test-optimization-mocha-latest-latest 53.45% <ø> (+0.08%) ⬆️
test-optimization-mocha-latest-oldest 51.03% <ø> (+0.08%) ⬆️
test-optimization-mocha-oldest-latest 53.58% <ø> (+0.10%) ⬆️
test-optimization-mocha-oldest-oldest 51.02% <ø> (+0.08%) ⬆️
test-optimization-playwright-latest-latest-playwright-active-test-span 44.10% <ø> (+0.26%) ⬆️
test-optimization-playwright-latest-latest-playwright-atr 42.91% <ø> (+0.10%) ⬆️
test-optimization-playwright-latest-latest-playwright-efd 43.33% <ø> (+0.08%) ⬆️
test-optimization-playwright-latest-latest-playwright-final-status 43.39% <ø> (-0.14%) ⬇️
test-optimization-playwright-latest-latest-playwright-impacted-tests ?
test-optimization-playwright-latest-latest-playwright-reporting 42.95% <ø> (+0.08%) ⬆️
test-optimization-playwright-latest-latest-playwright-test-management 44.40% <ø> (+0.10%) ⬆️
test-optimization-playwright-latest-oldest-playwright-active-test-span 44.06% <ø> (+0.26%) ⬆️
test-optimization-playwright-latest-oldest-playwright-atr 42.97% <ø> (+0.10%) ⬆️
test-optimization-playwright-latest-oldest-playwright-efd 43.26% <ø> (+0.09%) ⬆️
test-optimization-playwright-latest-oldest-playwright-final-status 43.33% <ø> (+0.10%) ⬆️
test-optimization-playwright-latest-oldest-playwright-impacted-tests 42.79% <ø> (ø)
test-optimization-playwright-latest-oldest-playwright-reporting 42.74% <ø> (+0.11%) ⬆️
test-optimization-playwright-latest-oldest-playwright-test-management 44.35% <ø> (?)
test-optimization-playwright-oldest-latest-playwright-active-test-span 44.32% <ø> (+0.08%) ⬆️
test-optimization-playwright-oldest-latest-playwright-atr 42.98% <ø> (+0.08%) ⬆️
test-optimization-playwright-oldest-latest-playwright-efd 43.38% <ø> (+0.08%) ⬆️
test-optimization-playwright-oldest-latest-playwright-final-status 43.44% <ø> (+0.06%) ⬆️
test-optimization-playwright-oldest-latest-playwright-impacted-tests 42.94% <ø> (ø)
test-optimization-playwright-oldest-latest-playwright-reporting ?
test-optimization-playwright-oldest-latest-playwright-test-management 44.48% <ø> (+0.08%) ⬆️
test-optimization-playwright-oldest-oldest-playwright-active-test-span 44.15% <ø> (+0.26%) ⬆️
test-optimization-playwright-oldest-oldest-playwright-atr 43.05% <ø> (+0.10%) ⬆️
test-optimization-playwright-oldest-oldest-playwright-efd 43.32% <ø> (+0.09%) ⬆️
test-optimization-playwright-oldest-oldest-playwright-final-status 43.41% <ø> (+0.10%) ⬆️
test-optimization-playwright-oldest-oldest-playwright-impacted-tests 42.87% <ø> (ø)
test-optimization-playwright-oldest-oldest-playwright-reporting 42.80% <ø> (+0.08%) ⬆️
test-optimization-playwright-oldest-oldest-playwright-test-management 44.43% <ø> (+0.10%) ⬆️
test-optimization-selenium-latest 45.39% <ø> (+0.06%) ⬆️
test-optimization-selenium-oldest 44.98% <ø> (+0.07%) ⬆️
test-optimization-testopt-active 48.12% <ø> (+0.12%) ⬆️
test-optimization-testopt-latest 48.11% <ø> (+0.12%) ⬆️
test-optimization-testopt-maintenance 47.99% <ø> (+0.13%) ⬆️
test-optimization-testopt-oldest 49.29% <ø> (?)
test-optimization-vitest-latest 50.79% <ø> (+0.10%) ⬆️
test-optimization-vitest-oldest 48.05% <ø> (+0.31%) ⬆️

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.

@dd-octo-sts

dd-octo-sts Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Overall package size

Self size: 6.34 MB
Deduped: 7.41 MB
No deduping: 7.41 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

@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Jun 25, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

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

@pr-commenter

pr-commenter Bot commented Jun 25, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-06-25 18:16:24

Comparing candidate commit f12fcdf in PR branch BridgeAR/2026-06-25-express-loopback-test-discovery with baseline commit 4638d8e in branch master.

📊 Benchmarking dashboard

Found 0 performance improvements and 0 performance regressions! Performance is the same for 1951 metrics, 14 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 [-146.523ms; +155.692ms] or [-5.602%; +5.952%]

scenario:appsec-appsec-enabled-26

  • unstable execution_time [-167.208ms; +169.779ms] or [-6.747%; +6.850%]

scenario:appsec-control-20

  • unstable execution_time [-131590.916µs; +131154.650µs] or [-7.674%; +7.648%]

scenario:appsec-control-24

  • unstable execution_time [-90.521ms; +93.571ms] or [-7.537%; +7.791%]

scenario:appsec-control-26

  • unstable execution_time [-86.912ms; +103.054ms] or [-7.376%; +8.746%]

scenario:dogstatsd-with-tags-20

  • unstable cpu_user_time [-289.614ms; +373.992ms] or [-5.785%; +7.471%]
  • unstable execution_time [-290.305ms; +368.371ms] or [-5.716%; +7.253%]

scenario:plugin-graphql-long-with-depth-and-collapse-off-24

  • unstable cpu_user_time [-415.374ms; +132.194ms] or [-9.109%; +2.899%]
  • unstable execution_time [-450.427ms; +143.142ms] or [-9.283%; +2.950%]
  • unstable max_rss_usage [-60.391MB; +21.234MB] or [-10.983%; +3.862%]

scenario:plugin-graphql-long-with-depth-off-20

  • unstable max_rss_usage [-14.621MB; +7.474MB] or [-10.551%; +5.394%]

scenario:plugin-graphql-long-with-depth-off-26

  • unstable max_rss_usage [-14.167MB; +3.254MB] or [-8.727%; +2.004%]

scenario:plugin-redis-mset-wide-24

  • unstable cpu_user_time [-120.796ms; +270.451ms] or [-3.182%; +7.124%]
  • unstable execution_time [-120.887ms; +269.849ms] or [-3.176%; +7.089%]

@BridgeAR
BridgeAR marked this pull request as ready for review June 25, 2026 20:26
@BridgeAR
BridgeAR requested a review from a team as a code owner June 25, 2026 20:26
@BridgeAR
BridgeAR requested review from wconti27 and removed request for a team June 25, 2026 20:26
})

withVersions(plugin, 'loopback', loopbackVersion => {
withVersions('express', 'loopback', loopbackVersion => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maybe withVersions should throw if no matches are returned? That change may uncover additional similar situations

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I like that suggestion! Yes!

@BridgeAR
BridgeAR merged commit 64bc811 into master Jun 25, 2026
850 of 851 checks passed
@BridgeAR
BridgeAR deleted the BridgeAR/2026-06-25-express-loopback-test-discovery branch June 25, 2026 20:59
BridgeAR added a commit that referenced this pull request Jun 25, 2026
A withVersions call whose module argument matches no loaded instrumentation
produced zero test cases and registered no suites, so the spec passed while
running nothing. Passing the plugin export instead of its integration name was
the silent trigger. Throw instead, so a misconfigured call fails loudly rather
than skipping its whole suite.

Refs: #9062 (comment)
BridgeAR added a commit that referenced this pull request Jun 25, 2026
Middleware tracing replaced `layer.handle` with a wrapper and re-exposed the
user handler through an enumerable `_datadog_orig` back-reference, the only
property loopback's `_findLayerByHandler` could follow to tag a layer with its
phase. That ties the tracer to loopback's private reflection heuristic, where
the property name and its enumerability are both load-bearing.

Wrap the host `Layer` prototype dispatch (`handle_request`/`handle_error`, and
`handleRequest`/`handleError` on router >=2) and read per-layer metadata from a
WeakMap, leaving `layer.handle` the user's function. loopback's first lookup
(`layer.handle === handler`) then matches with no tracer-specific contract, so
the back-reference, the `express-async-errors` `__handle` branch, and the
`_name` write-back onto the user handler are no longer needed. A synchronous
throw or a rejected promise becomes `next(error)` in the host dispatch, so the
wrapped `next` captures both without a tracer-side try/catch.

Refs: #9062
BridgeAR added a commit that referenced this pull request Jun 25, 2026
…9069)

* test(harness): fail withVersions when no installed module matches

A withVersions call whose module argument matches no loaded instrumentation
produced zero test cases and registered no suites, so the spec passed while
running nothing. Passing the plugin export instead of its integration name was
the silent trigger. Throw instead, so a misconfigured call fails loudly rather
than skipping its whole suite.

Refs: #9062 (comment)

* test(graphql): run the silently-skipped apollo-server-core suite

The suite passed the plugin export to withVersions instead of the 'graphql'
integration name, so externals.js never resolved apollo-server-core and it ran
zero tests. Switch to the integration name; the suite stays describe.skip as
before.

* test(opensearch): drop the never-instrumented bare opensearch module

The instrumentation only hooks @opensearch-project/opensearch; the bare
opensearch package has never been hooked, so withVersions resolved no
instrumentation for it and the suite ran zero tests for that entry. The new
guard turns that silent skip into a throw, so remove the dead module.
dd-octo-sts Bot pushed a commit that referenced this pull request Jun 26, 2026
This makes sure loopback's phase-based middleware sorting keeps working under
tracing. loopback's `_findLayerByHandler` maps an express layer back to the
user handler by scanning the layer handle's enumerable properties, then tags
the layer with its phase. Replacing `layer.handle` with the tracing wrapper hid
the original handler, so the lookup failed, the layer kept no phase, and an
`app.middleware('final', ...)` handler ran in insertion order ahead of the
route handlers instead of last. Restoring the `_datadog_orig` back-reference
that the router instrumentation rewrite dropped lets the lookup succeed again.

Refs: #643

* test(express): run the silently-skipped loopback suite

1. The block passed the plugin export to `withVersions` instead of the
   `'express'` integration name, so no installed version matched and the suite
   never ran. Switching to the name runs it.
2. Legacy loopback emits the deprecated `util._extend` warning at module load,
   which the harness deprecation guard turns into a thrown error; allowlist that
   one message so the modules load.
dd-octo-sts Bot pushed a commit that referenced this pull request Jun 26, 2026
…9069)

* test(harness): fail withVersions when no installed module matches

A withVersions call whose module argument matches no loaded instrumentation
produced zero test cases and registered no suites, so the spec passed while
running nothing. Passing the plugin export instead of its integration name was
the silent trigger. Throw instead, so a misconfigured call fails loudly rather
than skipping its whole suite.

Refs: #9062 (comment)

* test(graphql): run the silently-skipped apollo-server-core suite

The suite passed the plugin export to withVersions instead of the 'graphql'
integration name, so externals.js never resolved apollo-server-core and it ran
zero tests. Switch to the integration name; the suite stays describe.skip as
before.

* test(opensearch): drop the never-instrumented bare opensearch module

The instrumentation only hooks @opensearch-project/opensearch; the bare
opensearch package has never been hooked, so withVersions resolved no
instrumentation for it and the suite ran zero tests for that entry. The new
guard turns that silent skip into a throw, so remove the dead module.
@dd-octo-sts dd-octo-sts Bot mentioned this pull request Jun 26, 2026
IlyasShabi pushed a commit that referenced this pull request Jun 26, 2026
This makes sure loopback's phase-based middleware sorting keeps working under
tracing. loopback's `_findLayerByHandler` maps an express layer back to the
user handler by scanning the layer handle's enumerable properties, then tags
the layer with its phase. Replacing `layer.handle` with the tracing wrapper hid
the original handler, so the lookup failed, the layer kept no phase, and an
`app.middleware('final', ...)` handler ran in insertion order ahead of the
route handlers instead of last. Restoring the `_datadog_orig` back-reference
that the router instrumentation rewrite dropped lets the lookup succeed again.

Refs: #643

* test(express): run the silently-skipped loopback suite

1. The block passed the plugin export to `withVersions` instead of the
   `'express'` integration name, so no installed version matched and the suite
   never ran. Switching to the name runs it.
2. Legacy loopback emits the deprecated `util._extend` warning at module load,
   which the harness deprecation guard turns into a thrown error; allowlist that
   one message so the modules load.
IlyasShabi pushed a commit that referenced this pull request Jun 26, 2026
…9069)

* test(harness): fail withVersions when no installed module matches

A withVersions call whose module argument matches no loaded instrumentation
produced zero test cases and registered no suites, so the spec passed while
running nothing. Passing the plugin export instead of its integration name was
the silent trigger. Throw instead, so a misconfigured call fails loudly rather
than skipping its whole suite.

Refs: #9062 (comment)

* test(graphql): run the silently-skipped apollo-server-core suite

The suite passed the plugin export to withVersions instead of the 'graphql'
integration name, so externals.js never resolved apollo-server-core and it ran
zero tests. Switch to the integration name; the suite stays describe.skip as
before.

* test(opensearch): drop the never-instrumented bare opensearch module

The instrumentation only hooks @opensearch-project/opensearch; the bare
opensearch package has never been hooked, so withVersions resolved no
instrumentation for it and the suite ran zero tests for that entry. The new
guard turns that silent skip into a throw, so remove the dead module.
@dd-octo-sts dd-octo-sts Bot mentioned this pull request Jun 26, 2026
pabloerhard pushed a commit that referenced this pull request Jun 26, 2026
This makes sure loopback's phase-based middleware sorting keeps working under
tracing. loopback's `_findLayerByHandler` maps an express layer back to the
user handler by scanning the layer handle's enumerable properties, then tags
the layer with its phase. Replacing `layer.handle` with the tracing wrapper hid
the original handler, so the lookup failed, the layer kept no phase, and an
`app.middleware('final', ...)` handler ran in insertion order ahead of the
route handlers instead of last. Restoring the `_datadog_orig` back-reference
that the router instrumentation rewrite dropped lets the lookup succeed again.

Refs: #643

* test(express): run the silently-skipped loopback suite

1. The block passed the plugin export to `withVersions` instead of the
   `'express'` integration name, so no installed version matched and the suite
   never ran. Switching to the name runs it.
2. Legacy loopback emits the deprecated `util._extend` warning at module load,
   which the harness deprecation guard turns into a thrown error; allowlist that
   one message so the modules load.
pabloerhard pushed a commit that referenced this pull request Jun 26, 2026
…9069)

* test(harness): fail withVersions when no installed module matches

A withVersions call whose module argument matches no loaded instrumentation
produced zero test cases and registered no suites, so the spec passed while
running nothing. Passing the plugin export instead of its integration name was
the silent trigger. Throw instead, so a misconfigured call fails loudly rather
than skipping its whole suite.

Refs: #9062 (comment)

* test(graphql): run the silently-skipped apollo-server-core suite

The suite passed the plugin export to withVersions instead of the 'graphql'
integration name, so externals.js never resolved apollo-server-core and it ran
zero tests. Switch to the integration name; the suite stays describe.skip as
before.

* test(opensearch): drop the never-instrumented bare opensearch module

The instrumentation only hooks @opensearch-project/opensearch; the bare
opensearch package has never been hooked, so withVersions resolved no
instrumentation for it and the suite ran zero tests for that entry. The new
guard turns that silent skip into a throw, so remove the dead module.
tlhunter pushed a commit that referenced this pull request Jul 14, 2026
* refactor(express): trace middleware via layer prototype dispatch

Middleware tracing replaced `layer.handle` with a wrapper and re-exposed the
user handler through an enumerable `_datadog_orig` back-reference, the only
property loopback's `_findLayerByHandler` could follow to tag a layer with its
phase. That ties the tracer to loopback's private reflection heuristic, where
the property name and its enumerability are both load-bearing.

Wrap the host `Layer` prototype dispatch (`handle_request`/`handle_error`, and
`handleRequest`/`handleError` on router >=2) and read per-layer metadata from a
WeakMap, leaving `layer.handle` the user's function. loopback's first lookup
(`layer.handle === handler`) then matches with no tracer-specific contract, so
the back-reference, the `express-async-errors` `__handle` branch, and the
`_name` write-back onto the user handler are no longer needed. A synchronous
throw or a rejected promise becomes `next(error)` in the host dispatch, so the
wrapped `next` captures both without a tracer-side try/catch.

Refs: #9062

* fix(express): trace middleware on express <4.3.0 (no prototype dispatch)

express <4.3.0 has no `Layer.prototype.handle_request`/`handle_error`; the
router invokes `layer.handle` directly, so wrapping the prototype traced
nothing and dropped middleware, route-handler, and code-origin spans there.
Replace `layer.handle` in place (arity preserved so the host still routes error
handlers) when the layer exposes no dispatch method. express 4.3.0+, express 5,
the router package, and loopback keep `handle` pristine via the prototype wraps.

Also point the router-helper spec at the renamed `setLayerMeta` helper.

* test(router): cover express-async-errors on prototype-dispatch hosts

express-async-errors redefines `handle` as a getter/setter that stores the
wrapped handler in `__handle` and patches `handle` only, never `handle_request`.
On express 4.3.0+ the tracer's prototype-dispatch wrap therefore has to survive
that patch, the arity gate has to read the real 3-arg handler, and a rejected
async handler has to reach the wrapped `next` as `next(error)`. Pin that path so
a regression surfaces here instead of in a downstream tracer test.

* fix(express): restore error publishing for throwing middleware on express <4.6.0

On express <4.6.0 the router has no `Layer` prototype dispatch: it runs
`layer.handle` directly and, on a synchronous throw, catches outside the layer
and calls its own `next(error)` — never the wrapped `next` the tracer installed.
The prototype-dispatch rewrite dropped the legacy handle wrap's catch, so a
throwing middleware or route handler no longer published `middleware:error` /
`middleware:next` / `middleware:finish`: the span lost its error tag and lingered
on the stack until request finish.

Restore the catch in `wrapLegacyHandle` to publish error/next/finish and rethrow,
mirroring `wrapNext`. The prototype hosts (express >=4.6.0, express 5, router)
already route the throw through the wrapped next, so only the legacy path needs it.

Also correct the version boundary in the surrounding comments: `handle_request`
landed in express 4.6.0, not 4.3.0, so the legacy path covers `>=4.0.0 <4.6.0`.

Refs: #9067 (comment)

* fix(express): publish middleware next/finish once per continuation

A handler that calls `next()` and then rejects (`async (req, res, next) => {
next(); await bg() }` where `bg()` rejects) makes the host invoke the same
wrapped `next` twice: the clean `next()` finishes and pops the middleware span,
then the host's rejection pass calls `next(error)` again. Since the middleware
stack is already empty, that second pass tagged the error on the parent span,
inventing a request-level error the middleware never surfaced.

Guard the wrapped continuation so its error/next/finish publishes fire at most
once per dispatch; the second call still forwards to the host chain untouched.

Add the missing JSDoc on `hasLayerDispatch`.
juan-fernandez pushed a commit that referenced this pull request Jul 16, 2026
* refactor(express): trace middleware via layer prototype dispatch

Middleware tracing replaced `layer.handle` with a wrapper and re-exposed the
user handler through an enumerable `_datadog_orig` back-reference, the only
property loopback's `_findLayerByHandler` could follow to tag a layer with its
phase. That ties the tracer to loopback's private reflection heuristic, where
the property name and its enumerability are both load-bearing.

Wrap the host `Layer` prototype dispatch (`handle_request`/`handle_error`, and
`handleRequest`/`handleError` on router >=2) and read per-layer metadata from a
WeakMap, leaving `layer.handle` the user's function. loopback's first lookup
(`layer.handle === handler`) then matches with no tracer-specific contract, so
the back-reference, the `express-async-errors` `__handle` branch, and the
`_name` write-back onto the user handler are no longer needed. A synchronous
throw or a rejected promise becomes `next(error)` in the host dispatch, so the
wrapped `next` captures both without a tracer-side try/catch.

Refs: #9062

* fix(express): trace middleware on express <4.3.0 (no prototype dispatch)

express <4.3.0 has no `Layer.prototype.handle_request`/`handle_error`; the
router invokes `layer.handle` directly, so wrapping the prototype traced
nothing and dropped middleware, route-handler, and code-origin spans there.
Replace `layer.handle` in place (arity preserved so the host still routes error
handlers) when the layer exposes no dispatch method. express 4.3.0+, express 5,
the router package, and loopback keep `handle` pristine via the prototype wraps.

Also point the router-helper spec at the renamed `setLayerMeta` helper.

* test(router): cover express-async-errors on prototype-dispatch hosts

express-async-errors redefines `handle` as a getter/setter that stores the
wrapped handler in `__handle` and patches `handle` only, never `handle_request`.
On express 4.3.0+ the tracer's prototype-dispatch wrap therefore has to survive
that patch, the arity gate has to read the real 3-arg handler, and a rejected
async handler has to reach the wrapped `next` as `next(error)`. Pin that path so
a regression surfaces here instead of in a downstream tracer test.

* fix(express): restore error publishing for throwing middleware on express <4.6.0

On express <4.6.0 the router has no `Layer` prototype dispatch: it runs
`layer.handle` directly and, on a synchronous throw, catches outside the layer
and calls its own `next(error)` — never the wrapped `next` the tracer installed.
The prototype-dispatch rewrite dropped the legacy handle wrap's catch, so a
throwing middleware or route handler no longer published `middleware:error` /
`middleware:next` / `middleware:finish`: the span lost its error tag and lingered
on the stack until request finish.

Restore the catch in `wrapLegacyHandle` to publish error/next/finish and rethrow,
mirroring `wrapNext`. The prototype hosts (express >=4.6.0, express 5, router)
already route the throw through the wrapped next, so only the legacy path needs it.

Also correct the version boundary in the surrounding comments: `handle_request`
landed in express 4.6.0, not 4.3.0, so the legacy path covers `>=4.0.0 <4.6.0`.

Refs: #9067 (comment)

* fix(express): publish middleware next/finish once per continuation

A handler that calls `next()` and then rejects (`async (req, res, next) => {
next(); await bg() }` where `bg()` rejects) makes the host invoke the same
wrapped `next` twice: the clean `next()` finishes and pops the middleware span,
then the host's rejection pass calls `next(error)` again. Since the middleware
stack is already empty, that second pass tagged the error on the parent span,
inventing a request-level error the middleware never surfaced.

Guard the wrapped continuation so its error/next/finish publishes fire at most
once per dispatch; the second call still forwards to the host chain untouched.

Add the missing JSDoc on `hasLayerDispatch`.
juan-fernandez pushed a commit that referenced this pull request Jul 16, 2026
* refactor(express): trace middleware via layer prototype dispatch

Middleware tracing replaced `layer.handle` with a wrapper and re-exposed the
user handler through an enumerable `_datadog_orig` back-reference, the only
property loopback's `_findLayerByHandler` could follow to tag a layer with its
phase. That ties the tracer to loopback's private reflection heuristic, where
the property name and its enumerability are both load-bearing.

Wrap the host `Layer` prototype dispatch (`handle_request`/`handle_error`, and
`handleRequest`/`handleError` on router >=2) and read per-layer metadata from a
WeakMap, leaving `layer.handle` the user's function. loopback's first lookup
(`layer.handle === handler`) then matches with no tracer-specific contract, so
the back-reference, the `express-async-errors` `__handle` branch, and the
`_name` write-back onto the user handler are no longer needed. A synchronous
throw or a rejected promise becomes `next(error)` in the host dispatch, so the
wrapped `next` captures both without a tracer-side try/catch.

Refs: #9062

* fix(express): trace middleware on express <4.3.0 (no prototype dispatch)

express <4.3.0 has no `Layer.prototype.handle_request`/`handle_error`; the
router invokes `layer.handle` directly, so wrapping the prototype traced
nothing and dropped middleware, route-handler, and code-origin spans there.
Replace `layer.handle` in place (arity preserved so the host still routes error
handlers) when the layer exposes no dispatch method. express 4.3.0+, express 5,
the router package, and loopback keep `handle` pristine via the prototype wraps.

Also point the router-helper spec at the renamed `setLayerMeta` helper.

* test(router): cover express-async-errors on prototype-dispatch hosts

express-async-errors redefines `handle` as a getter/setter that stores the
wrapped handler in `__handle` and patches `handle` only, never `handle_request`.
On express 4.3.0+ the tracer's prototype-dispatch wrap therefore has to survive
that patch, the arity gate has to read the real 3-arg handler, and a rejected
async handler has to reach the wrapped `next` as `next(error)`. Pin that path so
a regression surfaces here instead of in a downstream tracer test.

* fix(express): restore error publishing for throwing middleware on express <4.6.0

On express <4.6.0 the router has no `Layer` prototype dispatch: it runs
`layer.handle` directly and, on a synchronous throw, catches outside the layer
and calls its own `next(error)` — never the wrapped `next` the tracer installed.
The prototype-dispatch rewrite dropped the legacy handle wrap's catch, so a
throwing middleware or route handler no longer published `middleware:error` /
`middleware:next` / `middleware:finish`: the span lost its error tag and lingered
on the stack until request finish.

Restore the catch in `wrapLegacyHandle` to publish error/next/finish and rethrow,
mirroring `wrapNext`. The prototype hosts (express >=4.6.0, express 5, router)
already route the throw through the wrapped next, so only the legacy path needs it.

Also correct the version boundary in the surrounding comments: `handle_request`
landed in express 4.6.0, not 4.3.0, so the legacy path covers `>=4.0.0 <4.6.0`.

Refs: #9067 (comment)

* fix(express): publish middleware next/finish once per continuation

A handler that calls `next()` and then rejects (`async (req, res, next) => {
next(); await bg() }` where `bg()` rejects) makes the host invoke the same
wrapped `next` twice: the clean `next()` finishes and pops the middleware span,
then the host's rejection pass calls `next(error)` again. Since the middleware
stack is already empty, that second pass tagged the error on the parent span,
inventing a request-level error the middleware never surfaced.

Guard the wrapped continuation so its error/next/finish publishes fire at most
once per dispatch; the second call still forwards to the host chain untouched.

Add the missing JSDoc on `hasLayerDispatch`.
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.

2 participants