Skip to content

Commit cd39df9

Browse files
authored
test: expand local integration harness to old-suite coverage parity (#821)
* test: expand local integration harness to old-suite coverage parity 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. * fix: normalize Node's racy once-per-process trace-warnings hint 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. * add manual datadog(handle) case where tracer isn't initialized. * local integration tests: stay runnable on dd-trace v6 trees 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. * normalize dns.addresses values
1 parent 965c99f commit cd39df9

60 files changed

Lines changed: 17826 additions & 176 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/integration_tests_local.yml‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ name: local integration tests
44
# against the real AWS Lambda base images + the Runtime Interface Emulator
55
# (RIE). No AWS account or credentials required.
66
#
7-
# One job per Node major version; each job builds and tests both container
8-
# variants (cjs, esm), which share the same ~1 GB base image, so each job
9-
# pulls it exactly once. If public.ecr.aws anonymous pull limits ever bite,
10-
# the fix is to mirror the base images to GHCR (same pattern dd-trace-js
11-
# uses in its mirror-image.yml workflow).
7+
# One job per Node major version; each job builds the container/layer fixture
8+
# images and runs every case (default: all of them) against that runtime.
9+
# The fixtures share the same ~1 GB base image, so each job pulls it exactly
10+
# once. If public.ecr.aws anonymous pull limits ever bite, the fix is to
11+
# mirror the base images to GHCR (same pattern dd-trace-js uses in its
12+
# mirror-image.yml workflow).
1213

1314
on:
1415
pull_request:

‎integration_tests/container/cjs/Dockerfile‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
ARG NODE_VERSION=22
22
FROM public.ecr.aws/lambda/nodejs:${NODE_VERSION}
33

4-
COPY package.json handler.js ${LAMBDA_TASK_ROOT}/
4+
# handler.js is the default redirect-mode fixture (DD_LAMBDA_HANDLER=handler.handle).
5+
# The remaining *.js files are the manual-wrap fixture handlers ported from the
6+
# AWS suite (throw-error, status-code-500s, send-metrics, process-input,
7+
# http-requests) plus extractor.js for the DD_TRACE_EXTRACTOR case.
8+
COPY package.json *.js ${LAMBDA_TASK_ROOT}/
59
COPY datadog-lambda-js-local.tgz /tmp/datadog-lambda-js-local.tgz
610
RUN cd ${LAMBDA_TASK_ROOT} \
711
&& npm install --omit=dev \
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Custom trace-context extractor fixture, loaded via
2+
// DD_TRACE_EXTRACTOR=extractor.extract (handler-style "module.function"
3+
// string, resolved by the shim's user-function loader).
4+
//
5+
// Returns a fixed context on every event: the extracted parent and the
6+
// `_dd.parent_source` span tag must show up in the trace, which the golden
7+
// asserts deterministically (ids are normalized to XXXX; the tag is not).
8+
module.exports.extract = async (event, context) => {
9+
return {
10+
traceId: "1234567890123456789",
11+
parentId: "9876543210987654321",
12+
sampleMode: 1,
13+
source: "event",
14+
};
15+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Manual-wrap companion to http-requests.js, run as the manual-http-requests
2+
// case — the exact shape of the AWS suite's http-requests function
3+
// (manual datadog() wrap, no userland dd-trace init).
4+
//
5+
// With no tracer initialized, TraceListener falls back to the library's own
6+
// patchHttp (src/trace/patch-http.ts), which wraps http/https and logs
7+
// "HTTP GET <url> TraceHeaders: [...]" per request. On real Lambda the
8+
// injected headers come from the platform's pass-through _X_AMZN_TRACE_ID;
9+
// under RIE that variable cannot be emulated (the RIC owns it), so the
10+
// golden pins the empty-context form (TraceHeaders: []) plus the mock echo
11+
// of the exact header set sent. The path under test — patchHttp wiring —
12+
// fails this golden the moment it stops wrapping or logging; the
13+
// context-dependent header values are pinned by the patch-http unit tests
14+
// and the AWS suite.
15+
const { datadog } = require("datadog-lambda-js");
16+
const { handle } = require("./http-requests");
17+
18+
module.exports.handle = datadog(handle);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Ported from integration_tests/http-requests.js with one seam adaptation:
2+
// the AWS suite hits the real ip-ranges.datadoghq.com/.eu endpoints; the
3+
// local harness is hermetic, so URLs come from MOCK_HTTP_URLS (set by run.sh
4+
// to the mock-http container on the case's docker network). The default keeps
5+
// this file runnable outside the harness.
6+
//
7+
// This entry point is UNWRAPPED and runs through the npm redirect entry
8+
// (dist/handler.handler) in the cjs-http-requests case. Redirect mode
9+
// initializes dd-trace first, so the tracer's http plugin patches axios and
10+
// the mock echo below shows the injected x-datadog-* headers in the golden.
11+
// The manual-wrap variant of the AWS suite's function — which exercises the
12+
// library's own patchHttp fallback instead — lives in http-requests-manual.js
13+
// and runs as the manual-http-requests case.
14+
//
15+
// The mock echoes the request headers it received, and this handler logs them,
16+
// so both goldens show the injected downstream trace context
17+
// (x-datadog-trace-id / x-datadog-parent-id, normalized to XXXX).
18+
const { sendDistributionMetric } = require("datadog-lambda-js");
19+
const axios = require("axios");
20+
21+
const urls = (process.env.MOCK_HTTP_URLS ||
22+
"https://ip-ranges.datadoghq.com,https://ip-ranges.datadoghq.eu"
23+
).split(",");
24+
25+
async function handle(event, context) {
26+
const responsePayload = { message: "hello, dog!" };
27+
28+
sendDistributionMetric("serverless.integration_test.execution", 1, "function:http-request");
29+
30+
for (let index = 0; index < urls.length; index++) {
31+
const response = await axios({ url: urls[index], method: "get" });
32+
console.log(`mock-http saw headers for ${urls[index]}: ${JSON.stringify(response.data.headers)}`);
33+
}
34+
35+
console.log(`Snapshot test http requests successfully made to URLs: ${urls}`);
36+
37+
return responsePayload;
38+
}
39+
40+
module.exports.handle = handle;

‎integration_tests/container/cjs/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "1.0.0",
44
"private": true,
55
"dependencies": {
6+
"axios": "1.18.1",
67
"dd-trace": "5.105.0"
78
}
89
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
const tracer = require("dd-trace").init();
2+
3+
const { datadog, sendDistributionMetric } = require("datadog-lambda-js");
4+
5+
// Minimal example of tracing to reduce flakiness
6+
async function handle(event, context) {
7+
const span = tracer.scope().active();
8+
9+
const responsePayload = { message: "hello, dog!" };
10+
11+
sendDistributionMetric("serverless.integration_test.execution", 1, "function:process-input-traced");
12+
13+
const { recordIds, eventType } = getRecordIds(event);
14+
15+
if (recordIds) {
16+
responsePayload.recordIds = recordIds;
17+
}
18+
19+
if (eventType) {
20+
responsePayload.eventType = eventType;
21+
} else {
22+
const requestId = getAPIGatewayRequestId(event);
23+
24+
if (requestId) {
25+
responsePayload.eventType = "APIGateway";
26+
27+
if (span) {
28+
span.setTag("api_gateway_request_id", requestId);
29+
}
30+
}
31+
}
32+
33+
if (span) {
34+
span.setTag("event_type", responsePayload.eventType);
35+
}
36+
37+
return responsePayload;
38+
}
39+
40+
const getRecordIds = tracer.wrap("getRecordIds", (event) => {
41+
const recordIds = [];
42+
let eventType = null;
43+
if (event.Records) {
44+
event.Records.forEach((record) => {
45+
if (record.messageId) {
46+
eventType = "SQS";
47+
recordIds.push(record.messageId);
48+
}
49+
if (record.Sns) {
50+
eventType = "SNS";
51+
recordIds.push(record.Sns.MessageId);
52+
}
53+
if (record.s3) {
54+
eventType = "S3";
55+
const bucketName = record.s3.bucket.name;
56+
const objectKey = record.s3.object.key;
57+
recordIds.push(`${bucketName}/${objectKey}`);
58+
}
59+
});
60+
}
61+
62+
if (eventType) {
63+
const span = tracer.scope().active();
64+
65+
if (span) {
66+
span.setTag("record_event_type", eventType);
67+
span.setTag("record_ids", recordIds.join());
68+
}
69+
}
70+
71+
return { recordIds, eventType };
72+
});
73+
74+
const getAPIGatewayRequestId = tracer.wrap("getAPIGatewayRequestId", (event) => {
75+
let requestId;
76+
77+
if (event.requestContext) {
78+
requestId = event.requestContext.requestId;
79+
80+
const span = tracer.scope().active();
81+
82+
if (span) {
83+
span.setTag("api_gateway_request_id", requestId);
84+
span.setTag("event_type", "APIGateway");
85+
}
86+
}
87+
88+
return requestId;
89+
});
90+
91+
module.exports.handle = datadog(handle);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const { datadog, sendDistributionMetric } = require("datadog-lambda-js");
2+
3+
sendDistributionMetric(
4+
"serverless.integration_test.outside_handler",
5+
1,
6+
"tagkey:tagvalue",
7+
`eventsource:outside_handler`,
8+
);
9+
10+
async function handle(event, context) {
11+
const responsePayload = { message: "hello, dog!" };
12+
13+
if (event.requestContext) {
14+
responsePayload.eventType = "APIGateway";
15+
responsePayload.requestId = event.requestContext.requestId;
16+
}
17+
18+
if (event.Records) {
19+
responsePayload.recordIds = [];
20+
21+
event.Records.forEach((record) => {
22+
if (record.messageId) {
23+
responsePayload.eventType = "SQS";
24+
responsePayload.recordIds.push(record.messageId);
25+
sendDistributionMetric(
26+
"serverless.integration_test.records_processed",
27+
1,
28+
"tagkey:tagvalue",
29+
`eventsource:${responsePayload.eventType}`,
30+
);
31+
}
32+
if (record.Sns) {
33+
responsePayload.eventType = "SNS";
34+
responsePayload.recordIds.push(record.Sns.MessageId);
35+
sendDistributionMetric(
36+
"serverless.integration_test.records_processed",
37+
1,
38+
"tagkey:tagvalue",
39+
`eventsource:${responsePayload.eventType}`,
40+
);
41+
}
42+
});
43+
}
44+
45+
sendDistributionMetric(
46+
"serverless.integration_test.execution",
47+
1,
48+
"tagkey:tagvalue",
49+
`eventsource:${responsePayload.eventType}`,
50+
);
51+
52+
console.log(`Processed ${responsePayload.eventType} request`);
53+
54+
return responsePayload;
55+
}
56+
57+
module.exports.handle = datadog(handle);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Ported as-is from integration_tests/status-code-500s.js (the AWS suite's
2+
// API Gateway 500 case): returns statusCode 500 so the span gets the error
3+
// tag and the aws.lambda.enhanced.errors metric is emitted.
4+
const tracer = require("dd-trace").init();
5+
const { datadog } = require("datadog-lambda-js");
6+
7+
async function handle(event, context) {
8+
const span = tracer.scope().active();
9+
return {
10+
"statusCode": 500,
11+
"body": {}
12+
};
13+
}
14+
15+
module.exports.handle = datadog(handle);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Ported as-is from integration_tests/throw-error-traced.js (the AWS suite's
2+
// manual-wrap error case): a handler that always throws, wrapped with
3+
// datadog() manually. Exercises thrown-error -> span error + rethrow through
4+
// the RIE invoke path.
5+
const { datadog } = require("datadog-lambda-js");
6+
7+
async function handle(event, context) {
8+
throw new Error("Hello");
9+
}
10+
module.exports.handle = datadog(handle);

0 commit comments

Comments
 (0)