Part of the audit remediation umbrella #67. Milestone 4. Severity: HIGH (first two items), MEDIUM (rest). Requirements: OBS-20, OBS-22, OBS-23, OBS-29, OBS-35, CTX-11, CTX-14, CTX-15, CTX-16.
Current SDK behavior
AsyncLocalStorage.enterWith leaks outward. pushDiagnosticFields
(observability/diagnostic-context.ts:91) and createAsyncScopedStore.enter (:118,125)
call enterWith. The restore closure runs after awaits inside send(), in a different
async resource, and never reaches the caller. After runtime.send() returns,
getActiveSpan() is the ended span and the process-wide diagnostic context still holds that
request's trace.id / span.id. An unrelated application log through any core Logger then
carries them. The existing tests assert inside the same continuation that called close().
- The operation span is suppressed after the first send.
startOperationSpan
(pipeline/runtime.ts:50) bails when getActiveSpan().isRecording. The leaked span still
reports isRecording === true, so only the first operation per process gets the
http.client.operation span.
- The operation span cannot be enabled at all from the public API.
PipelineBuilder.build()
(pipeline/builder.ts:241) calls createRuntime(flattened, transport) with no
contextInit; createRuntime is @internal; standardResilience() has no instrumentation
option. Every public runtime gets noopInstrumentationBundle. createInstrumentationBundle
is public with nowhere public to pass its result. The changeset
2026-09-04-per-operation-span.md shows createRuntime(...) in its example.
- A throwing
tracerFactory leaks the context-store entry. runtime.ts:170 installs the
request context before the try at :176; startOperationSpan and activateSpan (:174-175)
sit in the gap. store.size grows by one per failed send.
span.end() can run twice. If end() on the success path throws, the catch calls
recordException and end() again (runtime.ts:193,198-199).
- Body-drain failures are silent. The catches at
observability/logging-step.ts:309-312
and :333-335 return an empty capture and emit nothing. A fileBody() over a deleted file
logs "http.request.body.preview": "" with no http.instrumentation.* event.
DEXPACE_LOG_LEVEL is dead by default. resolveGranularity (logging-step.ts:84)
reads getGlobalConfiguration(), whose default is an empty configuration with an env seam of
() => undefined. Nothing in packages/ or docs/sdk-documentation/ installs
defaultConfiguration(). The key name is hardcoded (:85); OBS-35 says the SDK MUST NOT bake
in a default key name.
- Smaller:
noopInstrumentationBundle.activeSpan is undefined instead of NOOP_SPAN
(context/instrumentation.ts:65). operationName is never passed to promoteToRequest
(runtime.ts:169), so ctx.context.operationName is always undefined (CTX-16).
redactUrl(string) normalizes port, path and host case (redaction.ts:83; OBS-14).
logging-step.ts:427's span is ended inside executePipeline, not in the finally.
Expected behavior
When send() resolves or rejects, the active span and the diagnostic fields are what they
were before the call. Every operation gets its own http.client.operation span. A consumer
can pass an InstrumentationBundle through PipelineBuilder and standardResilience().
The context store has the same size after a failed send as before it. A body-drain failure
emits a best-effort http.instrumentation.* diagnostic. DEXPACE_LOG_LEVEL (or a
caller-chosen key) is read from the environment without extra wiring, or the required wiring
is documented.
Notes and leads
- Replace
enterWith plus restore closure with storage.run(value, fn) around the awaited
body of send(). run restores on exit and does not leak outward. Where a handle-based API
must stay, document that close() cannot restore the caller's context. Test: log after
await send() and assert no trace.id; assert the second send gets its own operation span.
- Public path: add an
instrumentation (or contextInit) option to PipelineBuilder's
constructor and to standardResilience()'s options, threaded to createRuntime. Pass
operationName through the same option (CTX-16). Fix the changeset example.
- Move
install inside the try, or open the try before the span start. Single exit for
span.end() guarded by an ended flag.
- Body-drain:
safeEmit an http.instrumentation.bodyCaptureFailed event with cause().
- Log level: either default the global configuration to
defaultConfiguration() (behavior
change, changeset) or document setGlobalConfiguration(defaultConfiguration()) in
pipelines.md. Add configKey to LoggingStepSettings.
- Land subtask 1 first:
tracerFactory's TSDoc and the deviations.md OBS-29 row change again
here.
- Minor changeset for
@dexpace/core (new builder option). api:local.
Part of the audit remediation umbrella #67. Milestone 4. Severity: HIGH (first two items), MEDIUM (rest). Requirements: OBS-20, OBS-22, OBS-23, OBS-29, OBS-35, CTX-11, CTX-14, CTX-15, CTX-16.
Current SDK behavior
AsyncLocalStorage.enterWithleaks outward.pushDiagnosticFields(
observability/diagnostic-context.ts:91) andcreateAsyncScopedStore.enter(:118,125)call
enterWith. The restore closure runs afterawaits insidesend(), in a differentasync resource, and never reaches the caller. After
runtime.send()returns,getActiveSpan()is the ended span and the process-wide diagnostic context still holds thatrequest's
trace.id/span.id. An unrelated application log through any coreLoggerthencarries them. The existing tests assert inside the same continuation that called
close().startOperationSpan(
pipeline/runtime.ts:50) bails whengetActiveSpan().isRecording. The leaked span stillreports
isRecording === true, so only the first operation per process gets thehttp.client.operationspan.PipelineBuilder.build()(
pipeline/builder.ts:241) callscreateRuntime(flattened, transport)with nocontextInit;createRuntimeis@internal;standardResilience()has no instrumentationoption. Every public runtime gets
noopInstrumentationBundle.createInstrumentationBundleis public with nowhere public to pass its result. The changeset
2026-09-04-per-operation-span.mdshowscreateRuntime(...)in its example.tracerFactoryleaks the context-store entry.runtime.ts:170installs therequest context before the
tryat:176;startOperationSpanandactivateSpan(:174-175)sit in the gap.
store.sizegrows by one per failed send.span.end()can run twice. Ifend()on the success path throws, thecatchcallsrecordExceptionandend()again (runtime.ts:193,198-199).observability/logging-step.ts:309-312and
:333-335return an empty capture and emit nothing. AfileBody()over a deleted filelogs
"http.request.body.preview": ""with nohttp.instrumentation.*event.DEXPACE_LOG_LEVELis dead by default.resolveGranularity(logging-step.ts:84)reads
getGlobalConfiguration(), whose default is an empty configuration with an env seam of() => undefined. Nothing inpackages/ordocs/sdk-documentation/installsdefaultConfiguration(). The key name is hardcoded (:85); OBS-35 says the SDK MUST NOT bakein a default key name.
noopInstrumentationBundle.activeSpanisundefinedinstead ofNOOP_SPAN(
context/instrumentation.ts:65).operationNameis never passed topromoteToRequest(
runtime.ts:169), soctx.context.operationNameis alwaysundefined(CTX-16).redactUrl(string)normalizes port, path and host case (redaction.ts:83; OBS-14).logging-step.ts:427's span is ended insideexecutePipeline, not in thefinally.Expected behavior
When
send()resolves or rejects, the active span and the diagnostic fields are what theywere before the call. Every operation gets its own
http.client.operationspan. A consumercan pass an
InstrumentationBundlethroughPipelineBuilderandstandardResilience().The context store has the same size after a failed send as before it. A body-drain failure
emits a best-effort
http.instrumentation.*diagnostic.DEXPACE_LOG_LEVEL(or acaller-chosen key) is read from the environment without extra wiring, or the required wiring
is documented.
Notes and leads
enterWithplus restore closure withstorage.run(value, fn)around the awaitedbody of
send().runrestores on exit and does not leak outward. Where a handle-based APImust stay, document that
close()cannot restore the caller's context. Test: log afterawait send()and assert notrace.id; assert the second send gets its own operation span.instrumentation(orcontextInit) option toPipelineBuilder'sconstructor and to
standardResilience()'s options, threaded tocreateRuntime. PassoperationNamethrough the same option (CTX-16). Fix the changeset example.installinside thetry, or open thetrybefore the span start. Single exit forspan.end()guarded by anendedflag.safeEmitanhttp.instrumentation.bodyCaptureFailedevent withcause().defaultConfiguration()(behaviorchange, changeset) or document
setGlobalConfiguration(defaultConfiguration())inpipelines.md. AddconfigKeytoLoggingStepSettings.tracerFactory's TSDoc and thedeviations.mdOBS-29 row change againhere.
@dexpace/core(new builder option).api:local.