Repository navigation
Description
Expected Behavior
On Node.js LMI, the default OTel integration should consume the header supplied as context.xRayTraceId and preserve its trace ID, parent and explicit sampling decision. It should retain ordinary Lambda compatibility and keep this data invocation-local.
Actual Behavior
The core wrapper does not forward context.xRayTraceId in InvocationInfo, and the default extractor reads only process.env._X_AMZN_TRACE_ID. Node.js LMI explicitly does not support that environment variable.
Using the published wrapper with a valid context-only header containing Sampled=0, the plugin receives no header, chooses a different ARN-derived trace ID, and exports sampled Invocation/Workflow spans when the fallback sampler is AlwaysOn. The same header supplied through the environment is correctly parsed as NOT_SAMPLED. This is a lost upstream-context input, rather than an issue with deterministic fallback IDs themselves.
Steps to Reproduce
Install @aws/[email protected], @aws/[email protected], @opentelemetry/[email protected], and @opentelemetry/[email protected]. Run the following with node repro.cjs. It invokes the real public wrapper using a valid local event, an LMI-shaped Lambda context and a service client that rejects any network operation.
Expected: the context header is available to the default extractor and Sampled=0 suppresses export. Observed: the captured invocation info has no context/header, the handler succeeds, and Invocation/Workflow are exported sampled on a different trace.
Reproduction code
const {withDurableExecution,DurableExecutionInvocationInputWithClient}=require('@aws/durable-execution-sdk-js');
const {ExecutionOtelPlugin}=require('@aws/durable-execution-sdk-js-otel');
const {NodeTracerProvider,InMemorySpanExporter,SimpleSpanProcessor,AlwaysOnSampler}=require('@opentelemetry/sdk-trace-node');
const header='Root=1-6955b900-123456789012345678901234;Parent=1234567890123456;Sampled=0';
const arn='arn:aws:lambda:us-west-2:123456789012:function:probe:1/durable-execution/probe/00000000-0000-4000-8000-000000000001';
const ctx={awsRequestId:'req',functionName:'probe',functionVersion:'1',invokedFunctionArn:'arn:aws:lambda:us-west-2:123456789012:function:probe:1',memoryLimitInMB:'128',logGroupName:'probe',logStreamName:'probe',getRemainingTimeInMillis:()=>30000,xRayTraceId:header,callbackWaitsForEmptyEventLoop:false};
const event={DurableExecutionArn:arn,CheckpointToken:'token',InitialExecutionState:{Operations:[{Id:arn.split('/').at(-1),Type:'EXECUTION',Status:'STARTED',StartTimestamp:new Date('2026-10-02T00:00:00Z'),ExecutionDetails:{InputPayload:'{}'}}]}};
const client=new Proxy({},{get:(_,name)=>()=>{throw new Error('unexpected network method '+String(name))}});
let infoSeen,provider;const exporter=new InMemorySpanExporter();
const plugin=new ExecutionOtelPlugin({tracerProviderFactory:create=>provider=new NodeTracerProvider({idGenerator:create(),sampler:new AlwaysOnSampler(),spanProcessors:[new SimpleSpanProcessor(exporter)]})});
const recorder={onInvocationStart:async info=>{infoSeen={keys:Object.keys(info),hasContext:'context' in info,xRayTraceId:info.xRayTraceId??null};}};
const wrapped=withDurableExecution(async()=> 'ok',{plugins:[recorder,plugin]});
(async()=>{
const old=process.env._X_AMZN_TRACE_ID;delete process.env._X_AMZN_TRACE_ID;
try {
const output=await wrapped(new DurableExecutionInvocationInputWithClient(event,client),ctx);
await provider.forceFlush();const result={inputContextTraceHeader:header,invocationInfo:infoSeen,output,spans:exporter.getFinishedSpans().map(s=>({name:s.name,traceId:s.spanContext().traceId,sampled:s.spanContext().traceFlags}))};
console.log(JSON.stringify(result,null,2));await provider.shutdown();
} finally {if(old===undefined)delete process.env._X_AMZN_TRACE_ID;else process.env._X_AMZN_TRACE_ID=old;}
})().catch(e=>{console.error(e);process.exitCode=1;});SDK Version
Core SDK 2.6.0; OTel plugin 1.1.0. Main rechecked at 2d285ddb54d07be3dcbdc09f7fd1696aece67659.
Node.js Version
24.21.0
Is this a regression?
Unknown; no last-known-good version has been established.
Additional Context
Sources: LMI request-context contract, core InvocationInfo construction, and default X-Ray extractor. The public Node.js runtime also skips the environment carrier in multi-concurrent mode.
This is reproduced locally against the documented runtime carrier, not a newly deployed LMI test. PR #924, checked at a2c9b2552071ec73ddb14d17d252fc7f56b0ba02, leaves this input path unchanged. Per-invocation plugin factories alone do not supply the missing header.
Acceptance criteria
- Forward the runtime header to the plugin/extractor and preserve Root, Parent and Sampled=0/1.
- Cover missing/invalid headers, the ordinary Lambda environment fallback, replay/resume, and overlapping LMI invocations with different headers.
- Do not copy invocation-local headers into shared environment variables.
- Add a deployed LMI test confirming trace correlation and upstream sampling.
Related cross-SDK tracking: aws/aws-durable-execution-sdk-java#762
Activity
zhongkechen commentedon Oct 22, 2026
Fixed on
mainby #924 (merge commit6597e0759d07abe792a167a0f31dcd7547eee736). The invocation-local X-Ray carrier implementation from #955 was incorporated into that PR; #955 was closed as incorporated, with no remaining source delta after the Core 3 plugin-factory adaptations.The SDK captures
context.xRayTraceIdin invocation-local metadata and passes it to the plugin/extractor, preserving Root, Parent, and explicit sampling. An available empty, nullish, invalid, or unreadable runtime carrier suppresses stale process-wide fallback; an unavailable carrier retains ordinary Lambda compatibility. No invocation header is written into shared environment variables. The merged implementation includes public-wrapper, carrier precedence, sampling, replay/resume, and compatibility coverage.Closing as completed for the implementation fix. The latest published GitHub SDK release, 2.7.1, does not contain this capability; it is part of the coordinated Core 3 / OTel 2 changes. Fresh composed Core 3 LMI lifecycle acceptance remains tracked separately in #1017 / #928 and is not claimed by this closure.