Skip to content

test: expand local integration harness to old-suite coverage parity - #821

Merged
joeyzhao2018 merged 8 commits into
mainfrom
joey/l2-coverage-parity
Sep 1, 2026
Merged

test: expand local integration harness to old-suite coverage parity#821
joeyzhao2018 merged 8 commits into
mainfrom
joey/l2-coverage-parity

Conversation

@joeyzhao2018

@joeyzhao2018 joeyzhao2018 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

What this is

PR 2a of the dd-trace migration roadmap (Phase 0, "freeze the oracle"). PR 1 (#804) built
the local Docker/RIE harness with two container cases; the roadmap's PR 2 was scoped as
bookkeeping (tag + docs). Reviewing that scope against migration_parity.md surfaced a
gap: the frozen goldens would have covered less than the AWS-based suite they are meant
to replace as the migration oracle. This PR widens the local harness so the frozen goldens
cover everything the old suite pinned — before any code moves.

It still moves no library code. It only adds fixtures, cases, and goldens, all captured
from the current implementation.

Coverage: old suite vs. this harness

The AWS suite (integration_tests/serverless.yml) deploys 8 handler variants × 9 events ×
5 runtimes. Every variant now has a docker-based counterpart:

AWS suite variant Local case Notes
container-cjs / container-esm container-cjs / container-esm unchanged from PR 1
layer-cjs / layer-esm layer-cjs / layer-esm new layer-mode fixture (below)
throw-error-traced manual-throw-error manual datadog() wrap, thrown error
status-code-500s-traced manual-status-500 manual wrap + userland dd-trace init, API GW 500
send-metrics manual-send-metrics sendDistributionMetric in/outside handler
process-input-traced manual-process-input manual wrap + userland dd-trace init
http-requests(-traced) cjs-http-requests redirect mode; injection via dd-trace's http plugin vs. hermetic mock
http-requests (manual-wrap shape) manual-http-requests manual wrap without userland init; the library's own patchHttp fallback
— (not covered before) cjs-custom-extractor DD_TRACE_EXTRACTOR, asserts _dd.parent_source: event
— (CI incident only) cjs-proactive-init proactive-initialization markers, gated

The last two rows close parity rows that migration_parity.md listed as Unprotected:
the custom-extractor path (_dd.parent_source) and proactive initialization are now pinned
by goldens + raw-log assertions instead of by hope. Still deliberately out of scope here
(left to unit/L3 as planned): DD_TRACE_DISABLED_INSTRUMENTATIONS=lambda, the ESM
double-registration guard, and direct-API/KMS paths.

What it took

Layer-mode fixture (integration_tests/container/layer/). The real layer ships a zip
mounted at /opt; locally we reproduce the load path, not the packaging:
prepare-layer.js assembles layer_pkg/ from the repo build in the release Dockerfile's
layout (dist/ + handler.mjs + the ESM module_importer overlay) and pins dependency
versions from the lockfile-resolved node_modules set, so the fixture drifts with the repo
instead of away from it. The dependency manifest mirrors the release build's full
dependency closure
— production dependencies plus every package
scripts/move_ddtrace_dependency.js moves (dd-trace, @datadog/native-appsec,
@datadog/pprof, @opentelemetry/api, @opentelemetry/api-logs) — so the local layer image has
the same dependency closure as the release layer. The image installs it at
/opt/nodejs/node_modules/datadog-lambda-js and sets the handler to
/opt/nodejs/node_modules/datadog-lambda-js/handler.handler, exactly like a layer user.
Build order matters: npm install must run before the fixture's COPY layer_pkg, or npm
prunes it.

Manual-wrap handlers, ported as-is from integration_tests/*.js into the cjs fixture,
with one finding worth writing down: manual-wrap cases without a userland
dd-trace.init() produce no traces
(No Tracer available, cannot start span) — and the
old AWS snapshots show the same (throw-error-traced/http-requests have traces=0 in
their assertions). So the absence of trace JSON in the new goldens is faithful parity, not
a local-harness gap; the cases still pin error metrics, return bodies, and log output.

cjs-http-requests runs in redirect mode, not manual wrap, for the reason above:
traces are what make injected downstream headers observable. The AWS suite hits real
endpoints; locally a mock server on a per-run docker network (the Lambda base image with
--entrypoint node) echoes received headers, so the golden pins x-datadog-*,
traceparent/tracestate, and log-injection fields deterministically. The fixture reads
MOCK_HTTP_URLS so the same handler code works in both suites.

manual-http-requests covers the other http-requests shape (review feedback): the AWS
suite's function is manually wrapped with no userland init, which routes outbound
injection through the library's own patchHttp fallback (src/trace/patch-http.ts),
gated on autoPatchHTTP && !tracerInitialized. Running this shape through the redirect
entry would skip that fallback entirely — a migration could break manual-wrapper HTTP
injection with every local test green. The golden pins what is observable under RIE
(patchHttp wraps and logs every request; the mock echo pins the exact header set); the
context-dependent header values remain pinned by the patch-http unit tests and the AWS
suite, because _X_AMZN_TRACE_ID cannot be emulated under RIE (documented in the harness
README's emulation-gaps section).

cjs-proactive-init promotes the PR 1 diagnostic (SIMULATE_PROACTIVE_INIT) into a
gated case: managed-instances eager init (AWS_LAMBDA_MAX_CONCURRENCY=1) + a 15 s
init→invoke gap, with the three markers ("proactive_initialization":1,
proactive_initialization:true, cold_start:false) grep-asserted on the raw logs so
normalization can never hide them. The old knobs remain as aliases
(VARIANT_PARAM=cjs|esm, SIMULATE_PROACTIVE_INIT=true).

Harness mechanics. run.sh gains a case engine (CASE_PARAM, bash-3.2-safe) and
per-case return-value modes (default / case / per-event) with most-specific-first
golden resolution. Assert-then-collapse is extended: the dd_lambda_layer:datadog-nodevNN
major is now verified against the runtime under test before collapsing to
nodevXX.XX.X, mirroring the runtime:nodejsNN.x check. normalize.sh adds scoped rules
for ISO8601-T RIC timestamps, compact-JSON trace headers, both requestId spellings,
TimeoutOverflowWarning values, (node:NN) pids, and the managed-instances supervisor
pid — each rule verified as a no-op against every previously committed golden.

Racy warning hint normalized. Node appends "(Use node --trace-warnings ...)" to the
first warning a process emits; under managed instances, which warning is first per
process is racy, so the hint's distribution across TimeoutOverflowWarning records differed
between runs — and between arm64 local and amd64 CI, which is what failed the node22/node24
legs mid-review. Normalization now strips the hint line wherever it appears.

dd-trace v6 trees stay runnable (forward compatibility)

The harness lives on main, so pull_request runs of other branches execute this
run.sh against their tree. On a tree that pins dd-trace v6 (engines.node >=22) with a
CI host on Node 18, the old pack step (yarn install --frozen-lockfile) fails before any
container builds. Two changes, both no-ops on this v5 tree:

  • the pack step installs through scripts/install_deps.sh with
    TARGET_NODE_MAJOR=$RUNTIME_PARAM when that script exists (v6 world: v5 fallback line
    for older runtimes, --ignore-engines for v6 on older hosts), falling back to plain
    yarn install otherwise;
  • container builds pass a DD_TRACE_VERSION build-arg resolved through
    scripts/dd_trace_versions.sh; this repo's fixture Dockerfiles declare no such ARG
    (docker only warns), while v6-world fixtures consume it to pin a tracer their runtime can
    run.

Verified against the actual v6 branch (onzia/moreddtrace6 + this harness): the node18 leg
installs dd-trace 5.124.0 and the node22 leg installs 6.12.0 on a Node 18 host — the exact
scenario that failed — containers build and run, and the only remaining diffs are the
expected tracer-version snapshot drift that the v6 PR owns regenerating.

Per-runtime divergences (pinned, not absorbed)

Real behavioral differences across Node majors get override goldens, per the harness's
"divergence is a new file" rule:

  • manual-throw-error_node{24,26}.{log,json} — Node 24+ add their own frames
    (BufferedInvokeProcessor, async index.mjs frames) to error stack traces; the node26
    preview RIC also embeds a per-invocation requestId in the error body (normalized).
  • cjs-http-requests_node18.log — dd-trace's dns/net plugins fire on Node 18 only,
    adding _dd.integration span meta.
  • manual-http-requests_node18.log — same Node-18-only dns/net plugin behavior on the
    fallback path.
  • cjs-proactive-init_node{18,20,24,26}.logTimeoutOverflowWarning emission (count
    and JSON-record wrapping) varies by major under managed instances. The case's actual
    assertions are the raw-log markers, identical on every runtime.

Verification

All 12 cases pass strict compare mode on nodejs 18/20/22/24/26 (60 legs), run twice where
goldens were re-captured. CI matrix is unchanged — the workflow comment now reflects that
each leg runs all cases.

Sequencing

PR 2b extends golden coverage beyond the old suite (callback-style handlers, fetch/undici
injection, DD_TRACE_ENABLED=false, DD_CAPTURE_LAMBDA_PAYLOAD) and lands after this one;
all goldens in both PRs are captured from pre-migration code.

Trying it locally

./integration_tests_local/run.sh                                   # all 5 runtimes, all 12 cases
RUNTIME_PARAM=18 CASE_PARAM=layer-esm ./integration_tests_local/run.sh
UPDATE_SNAPSHOTS=true RUNTIME_PARAM=22 CASE_PARAM=cjs-http-requests ./integration_tests_local/run.sh

Requires Docker. First run pulls the base images (~1 GB each) and the RIE binary.

PR 2a of the dd-trace migration roadmap: freeze goldens at the coverage
level of the AWS-based suite, not just the two container variants.

Cases (was 2, now 11): layer-cjs/layer-esm (new /opt layer fixture via
prepare-layer.js, lockfile-pinned deps), manual-throw-error /
manual-status-500 / manual-send-metrics / manual-process-input (AWS
suite's manual-wrap handlers ported into the cjs fixture),
cjs-http-requests (hermetic mock server on a per-run docker network),
cjs-custom-extractor (DD_TRACE_EXTRACTOR, asserts _dd.parent_source),
cjs-proactive-init (managed-instances eager init + 15s gap, raw-log
marker assertions).

Harness: run.sh case engine (CASE_PARAM; VARIANT_PARAM and
SIMULATE_PROACTIVE_INIT kept as aliases), per-case return-value modes
(default/case/per-event) with most-specific-first golden resolution;
assert-then-collapse extended to the dd_lambda_layer node major;
normalize.sh rules for ISO8601-T RIC timestamps, compact JSON trace
headers, requestId forms, TimeoutOverflowWarning values, node pids, and
the managed-instances supervisor pid.

Per-runtime override goldens pin genuine divergences: Node 24+/26 error
stack frames, Node 18 dns/net plugin spans, per-major
TimeoutOverflowWarning emission in the proactive case.

Verified: all 11 cases pass strict compare on nodejs 18/20/22/24/26.
@datadog-official

datadog-official Bot commented Aug 28, 2026

Copy link
Copy Markdown

Pipelines

Unblock PR with BitsAI

⚠️ Warnings

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 1 Pipeline job failed

DataDog/datadog-lambda-js | integration test (node18) — 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 480fd40 | Docs | View more details | Give us feedback!

Node appends "(Use `node --trace-warnings ...` to show where the warning
was created)" to the first warning a process emits. Under the
managed-instances path, which warning is first per process is racy, so the
hint's distribution across TimeoutOverflowWarning records differs between
runs — and between arm64 local and amd64 CI, which is what failed the
node22/node24 legs on the PR.

Drop the hint in both its forms (JSON-embedded `\n(...)` suffix and
standalone line) in normalize.sh and re-capture the four proactive-init
goldens that contain it. Verified: strict compare passes for
cjs-proactive-init on all five runtimes.
@joeyzhao2018
joeyzhao2018 marked this pull request as ready for review August 29, 2026 01:09
@joeyzhao2018
joeyzhao2018 requested review from a team as code owners August 29, 2026 01:10

@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: 6dd3d8e4bf

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread integration_tests_local/run.sh
Comment thread integration_tests_local/prepare-layer.js Outdated
The pack step ran `yarn install --frozen-lockfile` directly. On a tree that
pins dd-trace v6 (engines node >=22) with a host Node older than 22 — e.g.
this repo's GitHub Actions runner image, which sets up Node 18.12 — that
install fails with "The engine \"node\" is incompatible with this module",
so every local integration test leg on a v6 PR fails before any container
is built.

Two changes, both no-ops on the current v5 tree:

- Pack step: when scripts/install_deps.sh exists (v6 world), install
  through it with TARGET_NODE_MAJOR=$RUNTIME_PARAM (default 22) so the v5
  fallback line is installed for older runtimes and --ignore-engines
  covers v6 on older hosts. The script restores package.json/yarn.lock on
  exit, so the worktree stays clean. Without it, fall back to the previous
  plain yarn install.
- Container builds: pass a DD_TRACE_VERSION build-arg resolved through
  scripts/dd_trace_versions.sh (v5 line for node <22) so the fixture image
  installs a tracer its runtime can actually run. Current fixture
  Dockerfiles declare no such ARG, so docker only warns; v6-world fixtures
  consume it via npm pkg set.

Verified:
- v5 tree: RUNTIME_PARAM=22 CASE_PARAM=container-cjs passes end to end.
- v6 tree (onzia/moreddtrace6 + this harness): node18 leg installs
  dd-trace 5.124.0 and node22 leg installs 6.12.0 on a Node 18 host (the
  original failure scenario), containers build and run; the only remaining
  diffs are the expected tracer-version snapshot drift (5.118 -> 5.124/6.x
  drop empty "links": [] and populate dd_trace meta), which the v6 PR owns
  regenerating, same as it already did for the AWS snapshots.

@ojproductions ojproductions left a comment

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.

lgtm

@joeyzhao2018
joeyzhao2018 merged commit cd39df9 into main Sep 1, 2026
53 of 55 checks passed
@joeyzhao2018
joeyzhao2018 deleted the joey/l2-coverage-parity branch September 1, 2026 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants