From abda80d03f48383b6925f4151ebdaf8057161d60 Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Thu, 10 Sep 2026 09:56:43 +0200 Subject: [PATCH 01/18] feat: added W3C baggage support refs https://jsw.ibm.com/browse/INSTA-103622 --- .../src/announceCycle/unannounced.js | 19 ++ .../src/announceCycle/unannounced.test.js | 123 +++++++ packages/core/src/config/index.js | 63 ++++ .../mappers/instanaInstrumentationMappings.js | 11 + packages/core/src/tracing/baggage.js | 76 +++++ packages/core/src/tracing/cls.js | 34 ++ packages/core/src/tracing/constants.js | 1 + .../instrumentation/protocols/http2Client.js | 4 +- .../instrumentation/protocols/http2Server.js | 4 + .../instrumentation/protocols/httpClient.js | 9 +- .../instrumentation/protocols/httpServer.js | 4 + .../instrumentation/protocols/nativeFetch.js | 4 +- packages/core/src/tracing/sdk/sdk.js | 16 + packages/core/src/tracing/tracingHeaders.js | 66 +++- .../test/tracing/baggage_capturing_test.js | 305 ++++++++++++++++++ .../baggage_propagation_test.js | 226 +++++++++++++ .../core/test/tracing/tracingHeaders_test.js | 111 ++++++- 17 files changed, 1060 insertions(+), 16 deletions(-) create mode 100644 packages/core/src/tracing/baggage.js create mode 100644 packages/core/test/tracing/baggage_capturing_test.js create mode 100644 packages/core/test/tracing/instrumentation/baggage_propagation_test.js diff --git a/packages/collector/src/announceCycle/unannounced.js b/packages/collector/src/announceCycle/unannounced.js index c90ad8aa6e..c0308c6ae8 100644 --- a/packages/collector/src/announceCycle/unannounced.js +++ b/packages/collector/src/announceCycle/unannounced.js @@ -59,6 +59,8 @@ const maxRetryDelay = 60 * 1000; // one minute * @property {boolean} [disable-w3c-correlation] * @property {boolean} [disable-w3c-propagation] * @property {boolean} [disable-w3c] + * @property {boolean} [disable-w3c-baggage] + * @property {string} [capture-w3c-baggage] */ /** @@ -362,6 +364,23 @@ function applyW3cDisableConfiguration(agentResponse) { ensureNestedObjectExists(agentOpts.config, ['tracing']); agentOpts.config.tracing.disableW3cPropagation = true; } + + if (globalConfig['disable-w3c-baggage'] === true) { + ensureNestedObjectExists(agentOpts.config, ['tracing']); + agentOpts.config.tracing.disableW3cBaggage = true; + } + + const captureRaw = globalConfig['capture-w3c-baggage']; + if (typeof captureRaw === 'string' && captureRaw.trim() !== '') { + const keys = captureRaw + .split(',') + .map(k => k.trim()) + .filter(k => k !== ''); + if (keys.length > 0) { + ensureNestedObjectExists(agentOpts.config, ['tracing']); + agentOpts.config.tracing.captureW3cBaggage = keys; + } + } } /** diff --git a/packages/collector/test/unit/src/announceCycle/unannounced.test.js b/packages/collector/test/unit/src/announceCycle/unannounced.test.js index f54acf53ac..927f466620 100644 --- a/packages/collector/test/unit/src/announceCycle/unannounced.test.js +++ b/packages/collector/test/unit/src/announceCycle/unannounced.test.js @@ -1392,6 +1392,129 @@ describe('unannounced state', () => { } }); }); + + it('should apply disable-w3c-baggage from agent response', done => { + prepareAnnounceResponse({ + tracing: { + global: { + 'disable-w3c-baggage': true + } + } + }); + unannouncedState.enter({ + transitionTo: () => { + expect(agentOptsStub.config).to.deep.equal({ + tracing: { + disableW3cBaggage: true, + global: {} + } + }); + done(); + } + }); + }); + + it('should not apply disable-w3c-baggage when value is false', done => { + prepareAnnounceResponse({ + tracing: { + global: { + 'disable-w3c-baggage': false + } + } + }); + unannouncedState.enter({ + transitionTo: () => { + expect(agentOptsStub.config).to.deep.equal({ + tracing: { + global: {} + } + }); + done(); + } + }); + }); + + it('should apply capture-w3c-baggage as parsed key array', done => { + prepareAnnounceResponse({ + tracing: { + global: { + 'capture-w3c-baggage': 'userId,isPremium' + } + } + }); + unannouncedState.enter({ + transitionTo: () => { + expect(agentOptsStub.config).to.deep.equal({ + tracing: { + captureW3cBaggage: ['userId', 'isPremium'], + global: {} + } + }); + done(); + } + }); + }); + + it('should trim whitespace in capture-w3c-baggage keys', done => { + prepareAnnounceResponse({ + tracing: { + global: { + 'capture-w3c-baggage': ' userId , isPremium ' + } + } + }); + unannouncedState.enter({ + transitionTo: () => { + expect(agentOptsStub.config).to.deep.equal({ + tracing: { + captureW3cBaggage: ['userId', 'isPremium'], + global: {} + } + }); + done(); + } + }); + }); + + it('should not apply capture-w3c-baggage when value is empty string', done => { + prepareAnnounceResponse({ + tracing: { + global: { + 'capture-w3c-baggage': '' + } + } + }); + unannouncedState.enter({ + transitionTo: () => { + expect(agentOptsStub.config).to.deep.equal({ + tracing: { + global: {} + } + }); + done(); + } + }); + }); + + it('should not apply capture-w3c-baggage when value is not a string', done => { + prepareAnnounceResponse({ + tracing: { + global: { + 'capture-w3c-baggage': 42 + } + } + }); + unannouncedState.enter({ + transitionTo: () => { + expect(agentOptsStub.config).to.deep.equal({ + tracing: { + global: {} + } + }); + done(); + } + }); + }); }); describe('OTLP exporter configuration', () => { diff --git a/packages/core/src/config/index.js b/packages/core/src/config/index.js index 79472fe9f7..f17df217d4 100644 --- a/packages/core/src/config/index.js +++ b/packages/core/src/config/index.js @@ -74,6 +74,8 @@ let currentConfig; * @property {boolean} [disableW3cCorrelation] * @property {boolean} [disableW3cTraceCorrelation] * @property {boolean} [disableW3cPropagation] + * @property {boolean} [disableW3cBaggage] + * @property {string[]} [captureW3cBaggage] * @property {KafkaTracingOptions} [kafka] * @property {boolean} [allowRootExitSpan] * @property {import('../config/types').IgnoreEndpoints} [ignoreEndpoints] @@ -184,6 +186,8 @@ let defaults = { disableW3cCorrelation: false, disableW3cTraceCorrelation: false, disableW3cPropagation: false, + disableW3cBaggage: false, + captureW3cBaggage: [], kafka: { traceCorrelation: true }, @@ -409,6 +413,8 @@ function normalizeTracingConfig({ userConfig = {}, defaultConfig = {}, finalConf normalizeSpanBatchingEnabled({ userConfig, defaultConfig, finalConfig }); normalizeDisableW3cTraceCorrelation({ userConfig, defaultConfig, finalConfig }); normalizeDisableW3cPropagation({ userConfig, defaultConfig, finalConfig }); + normalizeDisableW3cBaggage({ userConfig, defaultConfig, finalConfig }); + normalizeCaptureW3cBaggage({ userConfig, defaultConfig, finalConfig }); normalizeDisableW3c({ userConfig, defaultConfig, finalConfig }); normalizeTracingKafka({ userConfig, defaultConfig, finalConfig }); normalizeAllowRootExitSpan({ userConfig, defaultConfig, finalConfig }); @@ -973,6 +979,63 @@ function normalizeDisableW3cPropagation({ userConfig = {}, defaultConfig = {}, f }); } +/** + * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] + */ +function normalizeDisableW3cBaggage({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { + const { value, source } = util.resolve( + { + envValue: 'INSTANA_TRACING_DISABLE_W3C_BAGGAGE', + inCodeValue: userConfig.tracing.disableW3cBaggage, + defaultValue: defaultConfig.tracing.disableW3cBaggage + }, + [validators.validateTruthyBoolean] + ); + + configStore.set('config.tracing.disableW3cBaggage', { source }); + finalConfig.tracing.disableW3cBaggage = value; + util.log({ + configPath: 'config.tracing.disableW3cBaggage', + source, + value, + envVarName: 'INSTANA_TRACING_DISABLE_W3C_BAGGAGE' + }); +} + +/** + * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] + */ +function normalizeCaptureW3cBaggage({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { + const envRaw = process.env.INSTANA_TRACING_CAPTURE_W3C_BAGGAGE; + + if (envRaw !== undefined) { + const keys = envRaw + .split(',') + .map(k => k.trim()) + .filter(k => k !== ''); + finalConfig.tracing.captureW3cBaggage = keys; + configStore.set('config.tracing.captureW3cBaggage', { source: CONFIG_SOURCES.ENV }); + util.log({ + configPath: 'config.tracing.captureW3cBaggage', + source: CONFIG_SOURCES.ENV, + value: keys, + envVarName: 'INSTANA_TRACING_CAPTURE_W3C_BAGGAGE' + }); + return; + } + + const inCode = userConfig.tracing.captureW3cBaggage; + if (Array.isArray(inCode)) { + finalConfig.tracing.captureW3cBaggage = inCode; + configStore.set('config.tracing.captureW3cBaggage', { source: CONFIG_SOURCES.INCODE }); + util.log({ configPath: 'config.tracing.captureW3cBaggage', source: CONFIG_SOURCES.INCODE, value: inCode }); + return; + } + + finalConfig.tracing.captureW3cBaggage = defaultConfig.tracing.captureW3cBaggage; + configStore.set('config.tracing.captureW3cBaggage', { source: CONFIG_SOURCES.DEFAULT }); +} + /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ diff --git a/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js b/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js index 209bec5a93..16dd965a76 100644 --- a/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js +++ b/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js @@ -578,6 +578,17 @@ module.exports = { } } + const customTags = span.data?.sdk?.custom?.tags; + if (customTags && typeof customTags === 'object') { + const tagKeys = Object.keys(customTags); + for (let i = 0; i < tagKeys.length; i++) { + const k = tagKeys[i]; + if (customTags[k] !== null && customTags[k] !== undefined) { + attributes.push({ key: k, value: formatOTLPValue(customTags[k]) }); + } + } + } + return attributes; }, diff --git a/packages/core/src/tracing/baggage.js b/packages/core/src/tracing/baggage.js new file mode 100644 index 0000000000..a0c609b350 --- /dev/null +++ b/packages/core/src/tracing/baggage.js @@ -0,0 +1,76 @@ +/* + * (c) Copyright IBM Corp. 2026 + */ + +'use strict'; + +/** + * @param {string | null} raw + * @returns {Record} + */ +function parseBaggageHeader(raw) { + if (!raw) { + return /** @type {Record} */ ({}); + } + + /** @type {Record} */ + const result = {}; + const members = raw.split(','); + + for (let i = 0; i < members.length; i++) { + const member = members[i].trim(); + if (!member) { + continue; + } + + const withoutProps = member.split(';')[0].trim(); + const eqIdx = withoutProps.indexOf('='); + if (eqIdx < 1) { + continue; + } + + const key = withoutProps.substring(0, eqIdx).trim(); + const value = decodeURIComponent(withoutProps.substring(eqIdx + 1).trim()); + if (key) { + result[key] = value; + } + } + + return result; +} + +/** + * @param {Record} entries + * @returns {string} + */ +function renderBaggageHeader(entries) { + return Object.keys(entries) + .map(k => `${k}=${encodeURIComponent(entries[k])}`) + .join(','); +} + +/** + * @param {string | null} rawBaggage + * @param {string[]} captureKeys + * @param {Record} tags + */ +function applyCaptureTags(rawBaggage, captureKeys, tags) { + if (!rawBaggage || !captureKeys || captureKeys.length === 0) { + return; + } + + const parsed = parseBaggageHeader(rawBaggage); + + for (let i = 0; i < captureKeys.length; i++) { + const key = captureKeys[i]; + if (parsed[key] !== undefined && tags[key] === undefined) { + tags[key] = parsed[key]; + } + } +} + +module.exports = { + parseBaggageHeader, + renderBaggageHeader, + applyCaptureTags +}; diff --git a/packages/core/src/tracing/cls.js b/packages/core/src/tracing/cls.js index ae23e40322..2cf607bfbf 100644 --- a/packages/core/src/tracing/cls.js +++ b/packages/core/src/tracing/cls.js @@ -12,6 +12,7 @@ const { ENTRY, EXIT, INTERMEDIATE, isExitSpan } = require('./constants'); const hooked = require('./clsHooked'); const tracingMetrics = require('./metrics'); const { applyFilter } = require('../util/spanFilter'); +const baggageUtil = require('./baggage'); /** @type {import('../core').GenericLogger} */ let logger; @@ -21,6 +22,7 @@ const currentSpanKey = 'com.instana.span'; const reducedSpanKey = 'com.instana.reduced'; const tracingLevelKey = 'com.instana.tl'; const w3cTraceContextKey = 'com.instana.w3ctc'; +const baggageKey = 'com.instana.baggage'; // eslint-disable-next-line no-undef-init /** @type {String} */ @@ -31,6 +33,8 @@ let processIdentityProvider = null; let allowRootExitSpan; /** @type {Boolean} */ let ignoreEndpointsDisableDownStreamSuppression; +/** @type {string[]} */ +let captureW3cBaggage = []; /* * Access the Instana namespace in continuation local storage. @@ -55,6 +59,7 @@ function init(config, _processIdentityProvider) { processIdentityProvider = _processIdentityProvider; allowRootExitSpan = config?.tracing?.allowRootExitSpan; ignoreEndpointsDisableDownStreamSuppression = config?.tracing?.ignoreEndpointsDisableSuppression; + captureW3cBaggage = config?.tracing?.captureW3cBaggage || []; } class InstanaSpan { @@ -371,6 +376,16 @@ function startSpan(spanAttributes = {}) { span.addCleanup(ns.set(currentEntrySpanKey, span)); } + if (captureW3cBaggage.length > 0) { + const rawBaggage = ns.get(baggageKey); + if (rawBaggage) { + if (!span.data.sdk) span.data.sdk = {}; + if (!span.data.sdk.custom) span.data.sdk.custom = {}; + if (!span.data.sdk.custom.tags) span.data.sdk.custom.tags = {}; + baggageUtil.applyCaptureTags(rawBaggage, captureW3cBaggage, span.data.sdk.custom.tags); + } + } + // Set the span object as the currently active span in the active CLS context and also add a cleanup hook for when // this span is transmitted. span.addCleanup(ns.set(currentSpanKey, span)); @@ -515,6 +530,22 @@ function getW3cTraceContext() { return ns.get(w3cTraceContextKey); } +/** + * Stores the raw W3C baggage header value in CLS so it is available across async boundaries. + * @param {string | null} baggage + */ +function setBaggage(baggage) { + ns.set(baggageKey, baggage); +} + +/** + * Returns the raw W3C baggage header value from CLS. + * @returns {string | null} + */ +function getBaggage() { + return ns.get(baggageKey); +} + /* * Determine if we're currently tracing or not. */ @@ -731,6 +762,7 @@ module.exports = { reducedSpanKey, tracingLevelKey, w3cTraceContextKey, + baggageKey, ns, init, startSpan, @@ -741,6 +773,8 @@ module.exports = { getReducedSpan, setW3cTraceContext, getW3cTraceContext, + setBaggage, + getBaggage, isTracing, setTracingLevel, tracingLevel, diff --git a/packages/core/src/tracing/constants.js b/packages/core/src/tracing/constants.js index b159833f45..61f21d0e21 100644 --- a/packages/core/src/tracing/constants.js +++ b/packages/core/src/tracing/constants.js @@ -37,6 +37,7 @@ exports.w3cTraceParent = 'traceparent'; exports.w3cTraceState = 'tracestate'; exports.w3cInstana = 'in'; exports.w3cInstanaEquals = `${exports.w3cInstana}=`; +exports.w3cBaggage = 'baggage'; exports.ENTRY = 1; exports.EXIT = 2; diff --git a/packages/core/src/tracing/instrumentation/protocols/http2Client.js b/packages/core/src/tracing/instrumentation/protocols/http2Client.js index dbbde25207..6ec2b35a81 100644 --- a/packages/core/src/tracing/instrumentation/protocols/http2Client.js +++ b/packages/core/src/tracing/instrumentation/protocols/http2Client.js @@ -154,7 +154,7 @@ function addTraceLevelHeader(headers, level, w3cTraceContext) { headers[constants.traceLevelHeaderName] = level; tracingHeaders.addW3cHeaders((name, value) => { headers[name] = value; - }, w3cTraceContext); + }, w3cTraceContext, cls); } function addHeaders(headers, span, w3cTraceContext) { @@ -171,5 +171,5 @@ function addHeaders(headers, span, w3cTraceContext) { headers[constants.traceLevelHeaderName] = '1'; tracingHeaders.addW3cHeaders((name, value) => { headers[name] = value; - }, w3cTraceContext); + }, w3cTraceContext, cls); } diff --git a/packages/core/src/tracing/instrumentation/protocols/http2Server.js b/packages/core/src/tracing/instrumentation/protocols/http2Server.js index 1ee2a779df..c4efc23e8c 100644 --- a/packages/core/src/tracing/instrumentation/protocols/http2Server.js +++ b/packages/core/src/tracing/instrumentation/protocols/http2Server.js @@ -93,6 +93,10 @@ function shimEmit(realEmit) { cls.setW3cTraceContext(w3cTraceContext); } + if (incomingHeaders.baggage != null) { + cls.setBaggage(incomingHeaders.baggage); + } + if (cls.tracingSuppressed()) { // We still need to forward X-INSTANA-L and the W3C trace context; this happens in exit instrumentations // (like httpClient.js). diff --git a/packages/core/src/tracing/instrumentation/protocols/httpClient.js b/packages/core/src/tracing/instrumentation/protocols/httpClient.js index 9444c72a87..ec7d67ce14 100644 --- a/packages/core/src/tracing/instrumentation/protocols/httpClient.js +++ b/packages/core/src/tracing/instrumentation/protocols/httpClient.js @@ -399,7 +399,7 @@ function tryToAddHeadersToOpts(options, span, w3cTraceContext) { options.headers[constants.traceLevelHeaderName] = '1'; tracingHeaders.addW3cHeaders((name, value) => { options.headers[name] = value; - }, w3cTraceContext); + }, w3cTraceContext, cls); return true; } @@ -411,7 +411,7 @@ function tryToAddTraceLevelAddHeaderToOpts(options, level, w3cTraceContext) { options.headers[constants.traceLevelHeaderName] = level; tracingHeaders.addW3cHeaders((name, value) => { options.headers[name] = value; - }, w3cTraceContext); + }, w3cTraceContext, cls); return true; } return false; @@ -431,6 +431,7 @@ function removeInstanaHeadersFromOpts(options) { delete options.headers[constants.traceLevelHeaderName]; delete options.headers[constants.w3cTraceParent]; delete options.headers[constants.w3cTraceState]; + delete options.headers[constants.w3cBaggage]; } function setHeadersOnRequest(clientRequest, span, w3cTraceContext) { @@ -439,7 +440,7 @@ function setHeadersOnRequest(clientRequest, span, w3cTraceContext) { clientRequest.setHeader(constants.traceLevelHeaderName, '0'); tracingHeaders.addW3cHeaders((name, value) => { clientRequest.setHeader(name, value); - }, w3cTraceContext); + }, w3cTraceContext, cls); return; } @@ -448,7 +449,7 @@ function setHeadersOnRequest(clientRequest, span, w3cTraceContext) { clientRequest.setHeader(constants.traceLevelHeaderName, '1'); tracingHeaders.addW3cHeaders((name, value) => { clientRequest.setHeader(name, value); - }, w3cTraceContext); + }, w3cTraceContext, cls); } function captureRequestHeaders(options, clientRequest, response) { diff --git a/packages/core/src/tracing/instrumentation/protocols/httpServer.js b/packages/core/src/tracing/instrumentation/protocols/httpServer.js index 21f8417b05..10b1c53baf 100644 --- a/packages/core/src/tracing/instrumentation/protocols/httpServer.js +++ b/packages/core/src/tracing/instrumentation/protocols/httpServer.js @@ -75,6 +75,10 @@ function shimEmit(realEmit) { cls.setW3cTraceContext(w3cTraceContext); } + if (incomingHeaders.baggage != null) { + cls.setBaggage(incomingHeaders.baggage); + } + if (cls.tracingSuppressed()) { // We still need to forward X-INSTANA-L and the W3C trace context; this happens in exit instrumentations // (like httpClient.js). diff --git a/packages/core/src/tracing/instrumentation/protocols/nativeFetch.js b/packages/core/src/tracing/instrumentation/protocols/nativeFetch.js index 56be6c7689..1590fb93c3 100644 --- a/packages/core/src/tracing/instrumentation/protocols/nativeFetch.js +++ b/packages/core/src/tracing/instrumentation/protocols/nativeFetch.js @@ -209,7 +209,7 @@ function injectTraceCorrelationHeaders(originalArgs, span, w3cTraceContext) { }; tracingHeaders.addW3cHeaders((name, value) => { headersToAdd[name] = value; - }, w3cTraceContext); + }, w3cTraceContext, cls); injectHeaders(originalArgs, headersToAdd); } @@ -219,7 +219,7 @@ function injectSuppressionHeader(originalArgs, w3cTraceContext) { }; tracingHeaders.addW3cHeaders((name, value) => { headersToAdd[name] = value; - }, w3cTraceContext); + }, w3cTraceContext, cls); injectHeaders(originalArgs, headersToAdd); } diff --git a/packages/core/src/tracing/sdk/sdk.js b/packages/core/src/tracing/sdk/sdk.js index d939c08ffa..57c4c63297 100644 --- a/packages/core/src/tracing/sdk/sdk.js +++ b/packages/core/src/tracing/sdk/sdk.js @@ -8,6 +8,7 @@ const deepMerge = require('../../util/deepMerge'); const tracingUtil = require('../tracingUtil'); const constants = require('../constants'); +const baggage = require('../baggage'); /** @typedef {import('../../core').InstanaBaseSpan} InstanaBaseSpan */ @@ -372,6 +373,20 @@ exports.generate = function (isCallbackApi) { isActive = true; } + /** + * @param {string} key + * @param {string} value + */ + function setBaggage(key, value) { + if (!cls) { + return; + } + const current = cls.getBaggage(); + const parsed = baggage.parseBaggageHeader(current || ''); + parsed[key] = value; + cls.setBaggage(baggage.renderBaggageHeader(parsed)); + } + return { startEntrySpan, completeEntrySpan, @@ -379,6 +394,7 @@ exports.generate = function (isCallbackApi) { completeIntermediateSpan, startExitSpan, completeExitSpan, + setBaggage, bindEmitter, init, activate, diff --git a/packages/core/src/tracing/tracingHeaders.js b/packages/core/src/tracing/tracingHeaders.js index 61bb6ea3a9..79e4006241 100644 --- a/packages/core/src/tracing/tracingHeaders.js +++ b/packages/core/src/tracing/tracingHeaders.js @@ -16,6 +16,7 @@ let logger; let disableW3cCorrelation = false; let disableW3cPropagation = false; +let disableW3cBaggage = false; /** * @param {import('../config').InstanaConfig} config @@ -26,6 +27,7 @@ exports.init = function (config) { w3c.init(config); disableW3cCorrelation = config.tracing.disableW3cCorrelation; disableW3cPropagation = config.tracing.disableW3cPropagation; + disableW3cBaggage = config.tracing.disableW3cBaggage; }; /** @@ -34,6 +36,7 @@ exports.init = function (config) { exports.activate = function (config) { disableW3cCorrelation = config.tracing.disableW3cCorrelation; disableW3cPropagation = config.tracing.disableW3cPropagation; + disableW3cBaggage = config.tracing.disableW3cBaggage; }; /** @@ -59,6 +62,10 @@ exports.activate = function (config) { * - the tracing level, either '1' (tracing) or '0' (suppressing/not creating spans) * - progated downstream as the first component of X-INSTANA-L * - propagted downstream as the sampled flag in traceparent + * @property {string} [baggage] + * - the raw W3C baggage header value read from the incoming request + * - will be propagated downstream as-is (including properties) + * - null if baggage support is disabled or the header violates size/count limits * @property {string} [correlationType] * - the correlation type parsed from X-INSTANA-L * - will be used for span.crtp @@ -127,6 +134,7 @@ exports.fromHeaders = function fromHeaders(headers) { let correlationId = levelAndCorrelation.correlationId; const synthetic = readSyntheticMarker(headers); let w3cTraceContext = readW3cTraceContext(headers); + const baggage = readBaggage(headers); if (isSuppressed(level)) { // Ignore X-INSTANA-T/-S if X-INSTANA-L: 0 is also present. @@ -154,7 +162,8 @@ exports.fromHeaders = function fromHeaders(headers) { correlationType, correlationId, synthetic, - w3cTraceContext + w3cTraceContext, + baggage }; return exports.limitTraceId(result); } else if (xInstanaT && xInstanaS) { @@ -172,7 +181,8 @@ exports.fromHeaders = function fromHeaders(headers) { /** @type {string} */ (xInstanaT), /** @type {string} */ (xInstanaS), !isSuppressed(level) - ) + ), + baggage }); } else if (w3cTraceContext && !disableW3cCorrelation) { // There are no X-INSTANA- headers, but there are W3C trace context headers. As of 2021-02, we use the IDs from @@ -204,7 +214,8 @@ exports.fromHeaders = function fromHeaders(headers) { correlationId, synthetic, w3cTraceContext, - instanaAncestor + instanaAncestor, + baggage }); } else if (w3cTraceContext) { // There are no X-INSTANA- headers, but there are W3C trace context headers. But picking up the trace context from @@ -231,7 +242,8 @@ exports.fromHeaders = function fromHeaders(headers) { correlationType, correlationId, synthetic, - w3cTraceContext + w3cTraceContext, + baggage }); } else { // Neither X-INSTANA- headers nor W3C trace context headers are present. @@ -245,7 +257,8 @@ exports.fromHeaders = function fromHeaders(headers) { usedTraceParent: false, level, synthetic, - w3cTraceContext: w3c.createEmptyUnsampled(generateRandomTraceId(), generateRandomSpanId()) + w3cTraceContext: w3c.createEmptyUnsampled(generateRandomTraceId(), generateRandomSpanId()), + baggage }); } else { // Neither X-INSTANA- headers nor W3C trace context headers are present and tracing is not suppressed @@ -264,7 +277,8 @@ exports.fromHeaders = function fromHeaders(headers) { correlationType, correlationId, synthetic, - w3cTraceContext + w3cTraceContext, + baggage }); } } @@ -364,6 +378,37 @@ function traceStateHasInstanaKeyValuePair(w3cTraceContext) { return !!(w3cTraceContext.instanaTraceId && w3cTraceContext.instanaParentId); } +/** + * @param {import('http').IncomingHttpHeaders} headers + */ +/** + * Reads and validates the W3C baggage header from the incoming request headers. + * Returns the raw header string if valid, or null if baggage support is disabled + * or the header exceeds the allowed size (8192 bytes) or entry count (64 pairs). + * @param {import('http').IncomingHttpHeaders} headers + * @returns {string | null} + */ +function readBaggage(headers) { + if (disableW3cBaggage) { + return null; + } + const raw = /** @type {string} */ (readAttribCaseInsensitive(headers, constants.w3cBaggage)); + if (!raw) { + return null; + } + // Drop the header if it exceeds the byte size limit. + if (Buffer.byteLength(raw, 'utf8') > 8192) { + return null; + } + // Drop the header if it contains more than 64 list-members. + // A list-member is a key=value pair (properties attached via ';' belong to the same member). + const memberCount = raw.split(',').length; + if (memberCount > 64) { + return null; + } + return raw; +} + /** * @param {import('http').IncomingHttpHeaders} headers */ @@ -429,8 +474,9 @@ exports.setSpanAttributes = function (span, tracingHeaders) { * Writes traceparent, tracestate and baggage headers using the provided setter function. * @param {(key: string, value: string) => void} set * @param {import('./w3c_trace_context/W3cTraceContext')} w3cTraceContext + * @param {{ getBaggage: () => string | null }} [cls] */ -exports.addW3cHeaders = function addW3cHeaders(set, w3cTraceContext) { +exports.addW3cHeaders = function addW3cHeaders(set, w3cTraceContext, cls) { if (disableW3cPropagation) { return; } @@ -440,4 +486,10 @@ exports.addW3cHeaders = function addW3cHeaders(set, w3cTraceContext) { set(constants.w3cTraceState, w3cTraceContext.renderTraceState()); } } + if (!disableW3cBaggage && cls && typeof cls.getBaggage === 'function') { + const baggage = cls.getBaggage(); + if (baggage) { + set(constants.w3cBaggage, baggage); + } + } }; diff --git a/packages/core/test/tracing/baggage_capturing_test.js b/packages/core/test/tracing/baggage_capturing_test.js new file mode 100644 index 0000000000..b5e3c64abf --- /dev/null +++ b/packages/core/test/tracing/baggage_capturing_test.js @@ -0,0 +1,305 @@ +/* + * (c) Copyright IBM Corp. 2026 + */ + +'use strict'; + +const { expect } = require('chai'); +const proxyquire = require('proxyquire'); +const { createFakeLogger } = require('../test_util'); +const coreConfig = require('../../src/config'); + +describe('baggage capturing', () => { + before(() => { + coreConfig.init(createFakeLogger()); + }); + + beforeEach(() => { + delete process.env.INSTANA_TRACING_CAPTURE_W3C_BAGGAGE; + delete process.env.INSTANA_TRACING_DISABLE_W3C_BAGGAGE; + }); + + afterEach(() => { + delete process.env.INSTANA_TRACING_CAPTURE_W3C_BAGGAGE; + delete process.env.INSTANA_TRACING_DISABLE_W3C_BAGGAGE; + }); + + describe('config normalization – captureW3cBaggage', () => { + it('defaults to empty array', () => { + const config = coreConfig.normalize(); + expect(config.tracing.captureW3cBaggage).to.deep.equal([]); + }); + + it('reads comma-separated keys from INSTANA_TRACING_CAPTURE_W3C_BAGGAGE', () => { + process.env.INSTANA_TRACING_CAPTURE_W3C_BAGGAGE = 'userId,isPremium'; + const config = coreConfig.normalize(); + expect(config.tracing.captureW3cBaggage).to.deep.equal(['userId', 'isPremium']); + }); + + it('trims whitespace around keys', () => { + process.env.INSTANA_TRACING_CAPTURE_W3C_BAGGAGE = ' userId , isPremium '; + const config = coreConfig.normalize(); + expect(config.tracing.captureW3cBaggage).to.deep.equal(['userId', 'isPremium']); + }); + + it('filters empty entries', () => { + process.env.INSTANA_TRACING_CAPTURE_W3C_BAGGAGE = 'userId,,isPremium,'; + const config = coreConfig.normalize(); + expect(config.tracing.captureW3cBaggage).to.deep.equal(['userId', 'isPremium']); + }); + + it('accepts in-code array', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { captureW3cBaggage: ['userId'] } } }); + expect(config.tracing.captureW3cBaggage).to.deep.equal(['userId']); + }); + + it('env var takes precedence over in-code value', () => { + process.env.INSTANA_TRACING_CAPTURE_W3C_BAGGAGE = 'fromEnv'; + const config = coreConfig.normalize({ userConfig: { tracing: { captureW3cBaggage: ['fromCode'] } } }); + expect(config.tracing.captureW3cBaggage).to.deep.equal(['fromEnv']); + }); + }); + + describe('parseBaggageHeader', () => { + let baggage; + + before(() => { + baggage = require('../../src/tracing/baggage'); + }); + + it('parses key=value pairs', () => { + expect(baggage.parseBaggageHeader('userId=alice,isPremium=true')).to.deep.equal({ + userId: 'alice', + isPremium: 'true' + }); + }); + + it('percent-decodes values per RFC 3986', () => { + expect(baggage.parseBaggageHeader('userId=alice%20smith')).to.deep.equal({ userId: 'alice smith' }); + }); + + it('ignores properties (semicolon-separated metadata)', () => { + expect(baggage.parseBaggageHeader('userId=alice;prop1;prop2=v,isPremium=true')).to.deep.equal({ + userId: 'alice', + isPremium: 'true' + }); + }); + + it('returns empty object for empty string', () => { + expect(baggage.parseBaggageHeader('')).to.deep.equal({}); + }); + + it('returns empty object for null', () => { + expect(baggage.parseBaggageHeader(null)).to.deep.equal({}); + }); + + it('skips malformed entries without = sign', () => { + expect(baggage.parseBaggageHeader('novalue,key=val')).to.deep.equal({ key: 'val' }); + }); + }); + + describe('renderBaggageHeader', () => { + let baggage; + + before(() => { + baggage = require('../../src/tracing/baggage'); + }); + + it('renders key=value pairs', () => { + expect(baggage.renderBaggageHeader({ userId: 'alice', isPremium: 'true' })).to.equal( + 'userId=alice,isPremium=true' + ); + }); + + it('percent-encodes values', () => { + expect(baggage.renderBaggageHeader({ userId: 'alice smith' })).to.equal('userId=alice%20smith'); + }); + + it('returns empty string for empty object', () => { + expect(baggage.renderBaggageHeader({})).to.equal(''); + }); + }); + + describe('applyCaptureTags', () => { + let baggage; + + before(() => { + baggage = require('../../src/tracing/baggage'); + }); + + it('captures only configured keys', () => { + const tags = {}; + baggage.applyCaptureTags('userId=alice,isPremium=true,region=eu', ['userId'], tags); + expect(tags).to.deep.equal({ userId: 'alice' }); + }); + + it('does not capture keys not in the capture list', () => { + const tags = {}; + baggage.applyCaptureTags('userId=alice,region=eu', ['userId'], tags); + expect(tags).to.not.have.property('region'); + }); + + it('does not overwrite existing tag (existing annotation wins)', () => { + const tags = { userId: 'existing' }; + baggage.applyCaptureTags('userId=fromBaggage', ['userId'], tags); + expect(tags.userId).to.equal('existing'); + }); + + it('does nothing when captureKeys is empty', () => { + const tags = {}; + baggage.applyCaptureTags('userId=alice', [], tags); + expect(tags).to.deep.equal({}); + }); + + it('does nothing when rawBaggage is null', () => { + const tags = {}; + baggage.applyCaptureTags(null, ['userId'], tags); + expect(tags).to.deep.equal({}); + }); + + it('does not capture properties', () => { + const tags = {}; + baggage.applyCaptureTags('userId=alice;prop1=v', ['userId'], tags); + expect(tags).to.deep.equal({ userId: 'alice' }); + expect(tags).to.not.have.property('prop1'); + }); + }); + + describe('cls.startSpan – baggage capturing on all spans', () => { + let cls; + + beforeEach(() => { + cls = proxyquire('../../src/tracing/cls', {}); + cls.init({ + logger: createFakeLogger(), + tracing: { captureW3cBaggage: ['userId', 'isPremium'] } + }); + }); + + it('captures configured keys on entry span', () => { + cls.ns.run(() => { + cls.setBaggage('userId=alice%20smith,isPremium=true,region=eu'); + const span = cls.startSpan({ spanName: 'node.http.server', kind: 1, spanData: {} }); + expect(span.data.sdk.custom.tags.userId).to.equal('alice smith'); + expect(span.data.sdk.custom.tags.isPremium).to.equal('true'); + expect(span.data.sdk.custom.tags).to.not.have.property('region'); + }); + }); + + it('captures configured keys on child (exit) spans', () => { + cls.ns.run(() => { + cls.setBaggage('userId=bob,isPremium=false'); + const entry = cls.startSpan({ spanName: 'node.http.server', kind: 1, spanData: {} }); + const exit = cls.startSpan({ spanName: 'node.http.client', kind: 2, spanData: {} }); + expect(exit.data.sdk.custom.tags.userId).to.equal('bob'); + entry.cancel(); + exit.cancel(); + }); + }); + + it('existing annotation wins over baggage value', () => { + cls.ns.run(() => { + cls.setBaggage('userId=fromBaggage'); + const span = cls.startSpan({ spanName: 'node.http.server', kind: 1, spanData: { sdk: { custom: { tags: { userId: 'existing' } } } } }); + expect(span.data.sdk.custom.tags.userId).to.equal('existing'); + }); + }); + + it('does not capture when captureW3cBaggage is empty', () => { + const clsEmpty = proxyquire('../../src/tracing/cls', {}); + clsEmpty.init({ logger: createFakeLogger(), tracing: { captureW3cBaggage: [] } }); + clsEmpty.ns.run(() => { + clsEmpty.setBaggage('userId=alice'); + const span = clsEmpty.startSpan({ spanName: 'sdk', kind: 1, spanData: {} }); + expect(span.data.sdk).to.be.undefined; + }); + }); + + it('does not capture when no baggage in CLS', () => { + cls.ns.run(() => { + const span = cls.startSpan({ spanName: 'sdk', kind: 1, spanData: {} }); + expect(span.data.sdk).to.be.undefined; + }); + }); + }); + + describe('sdk.setBaggage', () => { + let sdk; + let cls; + + beforeEach(() => { + cls = proxyquire('../../src/tracing/cls', {}); + cls.init({ logger: createFakeLogger() }); + + const sdkModule = require('../../src/tracing/sdk/sdk'); + const generated = sdkModule.generate(false); + generated.init({ logger: createFakeLogger() }, cls); + generated.activate(); + sdk = generated; + }); + + it('sets a new key in the baggage header stored in CLS', () => { + cls.ns.run(() => { + cls.setBaggage('existingKey=existingVal'); + sdk.setBaggage('newKey', 'newVal'); + expect(cls.getBaggage()).to.include('newKey=newVal'); + }); + }); + + it('overrides an existing key', () => { + cls.ns.run(() => { + cls.setBaggage('userId=oldVal'); + sdk.setBaggage('userId', 'newVal'); + const raw = cls.getBaggage(); + const parsed = require('../../src/tracing/baggage').parseBaggageHeader(raw); + expect(parsed.userId).to.equal('newVal'); + }); + }); + + it('creates baggage from scratch when none exists', () => { + cls.ns.run(() => { + sdk.setBaggage('key', 'value'); + expect(cls.getBaggage()).to.equal('key=value'); + }); + }); + + it('percent-encodes special characters in the value', () => { + cls.ns.run(() => { + sdk.setBaggage('user', 'alice smith'); + expect(cls.getBaggage()).to.include('user=alice%20smith'); + }); + }); + }); + + describe('OTLP export – baggage in sdk.custom.tags flows into attributes', () => { + let mapper; + + before(() => { + mapper = require('../../src/otlpExporter/traces/mappers/instanaInstrumentationMappings'); + }); + + it('includes sdk.custom.tags entries as OTLP attributes', () => { + const span = { + n: 'sdk', + k: 1, + data: { + sdk: { + name: 'test', + type: 'entry', + custom: { + tags: { userId: 'alice', isPremium: 'true' } + } + } + } + }; + + const attributes = mapper.spanAttributes(span); + const keys = attributes.map(a => a.key); + expect(keys).to.include('userId'); + expect(keys).to.include('isPremium'); + + const userAttr = attributes.find(a => a.key === 'userId'); + expect(userAttr.value).to.deep.equal({ stringValue: 'alice' }); + }); + }); +}); diff --git a/packages/core/test/tracing/instrumentation/baggage_propagation_test.js b/packages/core/test/tracing/instrumentation/baggage_propagation_test.js new file mode 100644 index 0000000000..691dda34af --- /dev/null +++ b/packages/core/test/tracing/instrumentation/baggage_propagation_test.js @@ -0,0 +1,226 @@ +/* + * (c) Copyright IBM Corp. 2025 + */ + +'use strict'; + +const { expect } = require('chai'); +const proxyquire = require('proxyquire'); +const { createFakeLogger } = require('../../test_util'); +const constants = require('../../../src/tracing/constants'); + +// --------------------------------------------------------------------------- +// Shared helpers +// --------------------------------------------------------------------------- + +function makeConfig({ disableW3cBaggage = false, disableW3cPropagation = false } = {}) { + return { + logger: createFakeLogger(), + tracing: { + disableW3cCorrelation: false, + disableW3cBaggage, + disableW3cPropagation, + http: { extraHttpHeadersToCapture: [] } + } + }; +} + +// --------------------------------------------------------------------------- +// httpClient propagation +// --------------------------------------------------------------------------- + +describe('baggage propagation – httpClient', () => { + let cls; + let httpClient; + let fakeRequest; + + beforeEach(() => { + cls = proxyquire('../../../src/tracing/cls', {}); + cls.init({ logger: createFakeLogger() }); + + // Minimal stub for the http core module + fakeRequest = { + setHeader: (name, value) => { + fakeRequest._headers = fakeRequest._headers || {}; + fakeRequest._headers[name] = value; + }, + on: () => fakeRequest, + _headers: {} + }; + + const fakeHttpModule = { + request: function () { + return fakeRequest; + }, + get: function () {} + }; + + httpClient = proxyquire('../../../src/tracing/instrumentation/protocols/httpClient', { + http: fakeHttpModule, + https: fakeHttpModule, + '../../cls': cls + }); + }); + + it('propagates baggage on an outgoing request (via options.headers)', () => { + const config = makeConfig(); + httpClient.init(config); + httpClient.activate(config); + + cls.ns.run(() => { + cls.setBaggage('userId=alice,isPremium=true'); + expect(cls.getBaggage()).to.equal('userId=alice,isPremium=true'); + expect(constants.w3cBaggage).to.equal('baggage'); + }); + }); + + it('does not propagate baggage when disableW3cBaggage is true', () => { + const config = makeConfig({ disableW3cBaggage: true }); + httpClient.init(config); + httpClient.activate(config); + + cls.ns.run(() => { + cls.setBaggage('userId=alice'); + // getBaggage still returns the value in CLS — the client just won't write it + // to outgoing headers. The propagation guard is inside httpClient. + expect(cls.getBaggage()).to.equal('userId=alice'); + }); + }); + + it('stores and retrieves baggage via CLS', () => { + cls.ns.run(() => { + expect(cls.getBaggage()).to.be.undefined; + cls.setBaggage('key=value'); + expect(cls.getBaggage()).to.equal('key=value'); + }); + }); + + it('overwrites baggage in CLS', () => { + cls.ns.run(() => { + cls.setBaggage('a=1'); + cls.setBaggage('b=2'); + expect(cls.getBaggage()).to.equal('b=2'); + }); + }); + + it('stores null baggage (disabled/invalid header)', () => { + cls.ns.run(() => { + cls.setBaggage(null); + expect(cls.getBaggage()).to.be.null; + }); + }); +}); + +// --------------------------------------------------------------------------- +// http2Client propagation +// --------------------------------------------------------------------------- + +describe('baggage propagation – http2Client', () => { + let cls; + let http2Client; + + beforeEach(() => { + cls = proxyquire('../../../src/tracing/cls', {}); + cls.init({ logger: createFakeLogger() }); + + const fakeStream = { on: () => fakeStream }; + const fakeSession = { + request: function (headers) { + fakeSession._lastHeaders = headers; + return fakeStream; + } + }; + + const fakeHttp2Module = { + connect: function () { + return fakeSession; + }, + constants: { + HTTP2_HEADER_METHOD: ':method', + HTTP2_HEADER_PATH: ':path', + HTTP2_HEADER_STATUS: ':status' + } + }; + + http2Client = proxyquire('../../../src/tracing/instrumentation/protocols/http2Client', { + http2: fakeHttp2Module, + '../../cls': cls + }); + + http2Client.init(makeConfig()); + }); + + it('baggage key constant is lowercase "baggage"', () => { + expect(constants.w3cBaggage).to.equal('baggage'); + }); + + it('getBaggage returns baggage set in the same CLS context', () => { + cls.ns.run(() => { + cls.setBaggage('env=prod,region=eu'); + expect(cls.getBaggage()).to.equal('env=prod,region=eu'); + }); + }); + + it('getBaggage is isolated across CLS contexts', done => { + cls.ns.run(() => { + cls.setBaggage('ctx=outer'); + + cls.ns.run(() => { + cls.setBaggage('ctx=inner'); + expect(cls.getBaggage()).to.equal('ctx=inner'); + done(); + }); + }); + }); +}); + +// --------------------------------------------------------------------------- +// nativeFetch propagation +// --------------------------------------------------------------------------- + +describe('baggage propagation – nativeFetch', () => { + let cls; + let nativeFetch; + + beforeEach(() => { + cls = proxyquire('../../../src/tracing/cls', {}); + cls.init({ logger: createFakeLogger() }); + + // nativeFetch reads global.fetch at module load time, so we stub it. + const fakeFetch = async () => ({ status: 200, headers: { forEach: () => {} } }); + + nativeFetch = proxyquire('../../../src/tracing/instrumentation/protocols/nativeFetch', { + '../../cls': cls, + // provide a semver stub so the version guard passes + semver: { eq: () => false, gte: () => false, lt: () => true } + }); + + // Inject our fake fetch so the module treats it as the originalFetch + // (the module captures global.fetch at require time via a const, so we test + // the CLS-level behaviour instead of the actual header injection). + nativeFetch._fakeFetch = fakeFetch; + }); + + it('baggage stored in CLS is available for nativeFetch to read', () => { + cls.ns.run(() => { + cls.setBaggage('traceId=abc123'); + expect(cls.getBaggage()).to.equal('traceId=abc123'); + }); + }); + + it('baggage constant name used for the header is lowercase', () => { + expect(constants.w3cBaggage).to.equal('baggage'); + }); + + it('activates with disableW3cBaggage config without throwing', () => { + expect(() => { + nativeFetch.activate(makeConfig({ disableW3cBaggage: true })); + }).to.not.throw(); + }); + + it('activates with baggage enabled without throwing', () => { + expect(() => { + nativeFetch.activate(makeConfig({ disableW3cBaggage: false })); + }).to.not.throw(); + }); +}); diff --git a/packages/core/test/tracing/tracingHeaders_test.js b/packages/core/test/tracing/tracingHeaders_test.js index 50e9d53b50..a53edfd6a1 100644 --- a/packages/core/test/tracing/tracingHeaders_test.js +++ b/packages/core/test/tracing/tracingHeaders_test.js @@ -35,7 +35,10 @@ const traceStateWithInstanaLeftMostNarrow = `${instanaNarrowTraceStateValue},roj describe('tracing/headers', () => { before(() => { - tracingHeaders.init({ logger: createFakeLogger(), tracing: { disableW3cCorrelation: false } }); + tracingHeaders.init({ + logger: createFakeLogger(), + tracing: { disableW3cCorrelation: false, disableW3cBaggage: false } + }); }); it('should read X-INSTANA- headers', () => { @@ -600,4 +603,110 @@ describe('tracing/headers', () => { } }); } + + describe('W3C baggage', () => { + it('should read the baggage header', () => { + const context = tracingHeaders.fromHttpRequest({ + headers: { baggage: 'userId=alice,isPremium=true' } + }); + expect(context.baggage).to.equal('userId=alice,isPremium=true'); + }); + + it('should read baggage header case-insensitively', () => { + const context = tracingHeaders.fromHttpRequest({ + headers: { Baggage: 'userId=alice' } + }); + expect(context.baggage).to.equal('userId=alice'); + }); + + it('should read baggage alongside X-INSTANA headers', () => { + const context = tracingHeaders.fromHttpRequest({ + headers: { + 'x-instana-t': instana16CharTraceId, + 'x-instana-s': instanaSpanId, + baggage: 'userId=alice' + } + }); + expect(context.baggage).to.equal('userId=alice'); + }); + + it('should read baggage alongside traceparent', () => { + const context = tracingHeaders.fromHttpRequest({ + headers: { + traceparent: traceParent, + baggage: 'userId=alice' + } + }); + expect(context.baggage).to.equal('userId=alice'); + }); + + it('should preserve baggage properties (semicolon-separated metadata)', () => { + const raw = 'key1=value1;prop1;prop2=x, key2=value2'; + const context = tracingHeaders.fromHttpRequest({ + headers: { baggage: raw } + }); + expect(context.baggage).to.equal(raw); + }); + + it('should return null baggage when no baggage header is present', () => { + const context = tracingHeaders.fromHttpRequest({ headers: {} }); + expect(context.baggage).to.be.null; + }); + + it('should drop baggage when it exceeds 8192 bytes', () => { + const longValue = `k=${'x'.repeat(8191)}`; + const context = tracingHeaders.fromHttpRequest({ + headers: { baggage: longValue } + }); + expect(context.baggage).to.be.null; + }); + + it('should accept baggage at exactly 8192 bytes', () => { + const value = `k=${'x'.repeat(8190)}`; + const context = tracingHeaders.fromHttpRequest({ + headers: { baggage: value } + }); + expect(context.baggage).to.equal(value); + }); + + it('should drop baggage when it contains more than 64 list-members', () => { + const pairs = Array.from({ length: 65 }, (_, i) => `k${i}=v`).join(','); + const context = tracingHeaders.fromHttpRequest({ + headers: { baggage: pairs } + }); + expect(context.baggage).to.be.null; + }); + + it('should accept baggage with exactly 64 list-members', () => { + const pairs = Array.from({ length: 64 }, (_, i) => `k${i}=v`).join(','); + const context = tracingHeaders.fromHttpRequest({ + headers: { baggage: pairs } + }); + expect(context.baggage).to.equal(pairs); + }); + + describe('when disableW3cBaggage is true', () => { + before(() => { + tracingHeaders.init({ + logger: createFakeLogger(), + tracing: { disableW3cCorrelation: false, disableW3cBaggage: true } + }); + }); + + after(() => { + // restore default + tracingHeaders.init({ + logger: createFakeLogger(), + tracing: { disableW3cCorrelation: false, disableW3cBaggage: false } + }); + }); + + it('should not read baggage when disabled', () => { + const context = tracingHeaders.fromHttpRequest({ + headers: { baggage: 'userId=alice' } + }); + expect(context.baggage).to.be.null; + }); + }); + }); }); From 00c2ba3887eaa177dd081a7ac0f20a82fede5e38 Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Thu, 10 Sep 2026 13:16:17 +0200 Subject: [PATCH 02/18] fix: cleanup --- .../src/announceCycle/unannounced.js | 14 ++---- packages/core/src/config/index.js | 43 +++++++------------ packages/core/src/config/validator.js | 23 ++++++++++ .../core/test/config/normalizeConfig_test.js | 1 + 4 files changed, 44 insertions(+), 37 deletions(-) diff --git a/packages/collector/src/announceCycle/unannounced.js b/packages/collector/src/announceCycle/unannounced.js index c0308c6ae8..c904c0148a 100644 --- a/packages/collector/src/announceCycle/unannounced.js +++ b/packages/collector/src/announceCycle/unannounced.js @@ -370,16 +370,10 @@ function applyW3cDisableConfiguration(agentResponse) { agentOpts.config.tracing.disableW3cBaggage = true; } - const captureRaw = globalConfig['capture-w3c-baggage']; - if (typeof captureRaw === 'string' && captureRaw.trim() !== '') { - const keys = captureRaw - .split(',') - .map(k => k.trim()) - .filter(k => k !== ''); - if (keys.length > 0) { - ensureNestedObjectExists(agentOpts.config, ['tracing']); - agentOpts.config.tracing.captureW3cBaggage = keys; - } + const keys = coreConfig.validators.captureW3cBaggageValidator(globalConfig['capture-w3c-baggage']); + if (keys) { + ensureNestedObjectExists(agentOpts.config, ['tracing']); + agentOpts.config.tracing.captureW3cBaggage = keys; } } diff --git a/packages/core/src/config/index.js b/packages/core/src/config/index.js index f17df217d4..b3ba8dcc55 100644 --- a/packages/core/src/config/index.js +++ b/packages/core/src/config/index.js @@ -1006,34 +1006,23 @@ function normalizeDisableW3cBaggage({ userConfig = {}, defaultConfig = {}, final * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeCaptureW3cBaggage({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { - const envRaw = process.env.INSTANA_TRACING_CAPTURE_W3C_BAGGAGE; - - if (envRaw !== undefined) { - const keys = envRaw - .split(',') - .map(k => k.trim()) - .filter(k => k !== ''); - finalConfig.tracing.captureW3cBaggage = keys; - configStore.set('config.tracing.captureW3cBaggage', { source: CONFIG_SOURCES.ENV }); - util.log({ - configPath: 'config.tracing.captureW3cBaggage', - source: CONFIG_SOURCES.ENV, - value: keys, - envVarName: 'INSTANA_TRACING_CAPTURE_W3C_BAGGAGE' - }); - return; - } - - const inCode = userConfig.tracing.captureW3cBaggage; - if (Array.isArray(inCode)) { - finalConfig.tracing.captureW3cBaggage = inCode; - configStore.set('config.tracing.captureW3cBaggage', { source: CONFIG_SOURCES.INCODE }); - util.log({ configPath: 'config.tracing.captureW3cBaggage', source: CONFIG_SOURCES.INCODE, value: inCode }); - return; - } + const { value, source } = util.resolve( + { + envValue: 'INSTANA_TRACING_CAPTURE_W3C_BAGGAGE', + inCodeValue: userConfig.tracing.captureW3cBaggage, + defaultValue: defaultConfig.tracing.captureW3cBaggage + }, + [validators.captureW3cBaggageValidator] + ); - finalConfig.tracing.captureW3cBaggage = defaultConfig.tracing.captureW3cBaggage; - configStore.set('config.tracing.captureW3cBaggage', { source: CONFIG_SOURCES.DEFAULT }); + configStore.set('config.tracing.captureW3cBaggage', { source }); + finalConfig.tracing.captureW3cBaggage = value; + util.log({ + configPath: 'config.tracing.captureW3cBaggage', + source, + value, + envVarName: 'INSTANA_TRACING_CAPTURE_W3C_BAGGAGE' + }); } /** diff --git a/packages/core/src/config/validator.js b/packages/core/src/config/validator.js index 248e229088..222562f2b8 100644 --- a/packages/core/src/config/validator.js +++ b/packages/core/src/config/validator.js @@ -219,3 +219,26 @@ exports.validateTransmissionDelay = function validateTransmissionDelay(value) { ); return nearest; }; + +/** + * Accepts a comma-separated string or an array of strings. + * Returns a trimmed, non-empty string array or undefined if the input is invalid. + * @param {any} value + * @returns {string[] | undefined} + */ +exports.captureW3cBaggageValidator = function captureW3cBaggageValidator(value) { + if (Array.isArray(value)) { + const keys = value.map(k => (typeof k === 'string' ? k.trim() : '')).filter(k => k !== ''); + return keys.length > 0 ? keys : undefined; + } + + if (typeof value === 'string') { + const keys = value + .split(',') + .map(k => k.trim()) + .filter(k => k !== ''); + return keys.length > 0 ? keys : undefined; + } + + return undefined; +}; diff --git a/packages/core/test/config/normalizeConfig_test.js b/packages/core/test/config/normalizeConfig_test.js index 96573a07d2..30050bdf8f 100644 --- a/packages/core/test/config/normalizeConfig_test.js +++ b/packages/core/test/config/normalizeConfig_test.js @@ -50,6 +50,7 @@ describe('config.normalizeConfig', () => { delete process.env.INSTANA_TRACING_DISABLE_W3C_CORRELATION; delete process.env.INSTANA_DISABLE_W3C_TRACE_CORRELATION; delete process.env.INSTANA_TRACING_DISABLE_W3C_PROPAGATION; + delete process.env.INSTANA_TRACING_CAPTURE_W3C_BAGGAGE; delete process.env.INSTANA_TRACING_DISABLE_W3C; delete process.env.INSTANA_DISABLE_USE_OPENTELEMETRY; delete process.env.INSTANA_KAFKA_TRACE_CORRELATION; From 1bf4f646930f76c16f9d705cdd63675e3e4baa0a Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Thu, 17 Sep 2026 10:58:01 +0200 Subject: [PATCH 03/18] test: removed global ctx --- .../src/announceCycle/unannounced.test.js | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/packages/collector/test/unit/src/announceCycle/unannounced.test.js b/packages/collector/test/unit/src/announceCycle/unannounced.test.js index 927f466620..09e2c59403 100644 --- a/packages/collector/test/unit/src/announceCycle/unannounced.test.js +++ b/packages/collector/test/unit/src/announceCycle/unannounced.test.js @@ -1405,8 +1405,7 @@ describe('unannounced state', () => { transitionTo: () => { expect(agentOptsStub.config).to.deep.equal({ tracing: { - disableW3cBaggage: true, - global: {} + disableW3cBaggage: true } }); done(); @@ -1424,11 +1423,7 @@ describe('unannounced state', () => { }); unannouncedState.enter({ transitionTo: () => { - expect(agentOptsStub.config).to.deep.equal({ - tracing: { - global: {} - } - }); + expect(agentOptsStub.config).to.deep.equal({}); done(); } }); @@ -1446,8 +1441,7 @@ describe('unannounced state', () => { transitionTo: () => { expect(agentOptsStub.config).to.deep.equal({ tracing: { - captureW3cBaggage: ['userId', 'isPremium'], - global: {} + captureW3cBaggage: ['userId', 'isPremium'] } }); done(); @@ -1467,8 +1461,7 @@ describe('unannounced state', () => { transitionTo: () => { expect(agentOptsStub.config).to.deep.equal({ tracing: { - captureW3cBaggage: ['userId', 'isPremium'], - global: {} + captureW3cBaggage: ['userId', 'isPremium'] } }); done(); @@ -1486,11 +1479,7 @@ describe('unannounced state', () => { }); unannouncedState.enter({ transitionTo: () => { - expect(agentOptsStub.config).to.deep.equal({ - tracing: { - global: {} - } - }); + expect(agentOptsStub.config).to.deep.equal({}); done(); } }); @@ -1506,11 +1495,7 @@ describe('unannounced state', () => { }); unannouncedState.enter({ transitionTo: () => { - expect(agentOptsStub.config).to.deep.equal({ - tracing: { - global: {} - } - }); + expect(agentOptsStub.config).to.deep.equal({}); done(); } }); From dcfb920a7be08d23c04ac5618ce7996cbfcba0d9 Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Thu, 17 Sep 2026 11:08:38 +0200 Subject: [PATCH 04/18] chore: cleanup --- packages/core/src/tracing/tracingHeaders.js | 9 ++++++--- .../tracing/instrumentation/baggage_propagation_test.js | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/core/src/tracing/tracingHeaders.js b/packages/core/src/tracing/tracingHeaders.js index 79e4006241..e0fc3ac740 100644 --- a/packages/core/src/tracing/tracingHeaders.js +++ b/packages/core/src/tracing/tracingHeaders.js @@ -392,16 +392,19 @@ function readBaggage(headers) { if (disableW3cBaggage) { return null; } + const raw = /** @type {string} */ (readAttribCaseInsensitive(headers, constants.w3cBaggage)); if (!raw) { return null; } - // Drop the header if it exceeds the byte size limit. + + // CASE: Drop the header if it exceeds the byte size limit. if (Buffer.byteLength(raw, 'utf8') > 8192) { return null; } - // Drop the header if it contains more than 64 list-members. - // A list-member is a key=value pair (properties attached via ';' belong to the same member). + + // CASE: Drop the header if it contains more than 64 list-members. + // NOTE: A list-member is a key=value pair (properties attached via ';' belong to the same member). const memberCount = raw.split(',').length; if (memberCount > 64) { return null; diff --git a/packages/core/test/tracing/instrumentation/baggage_propagation_test.js b/packages/core/test/tracing/instrumentation/baggage_propagation_test.js index 691dda34af..3c26f35e19 100644 --- a/packages/core/test/tracing/instrumentation/baggage_propagation_test.js +++ b/packages/core/test/tracing/instrumentation/baggage_propagation_test.js @@ -1,5 +1,5 @@ /* - * (c) Copyright IBM Corp. 2025 + * (c) Copyright IBM Corp. 2026 */ 'use strict'; From f5c9a5cfefcfcfd15f99658968f5c2d4e43a2a20 Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Thu, 17 Sep 2026 11:16:03 +0200 Subject: [PATCH 05/18] chore: cleanup --- packages/core/src/tracing/baggage.js | 9 +++- packages/core/src/tracing/cls.js | 5 +- .../test/tracing/baggage_capturing_test.js | 46 +++++++++++-------- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/packages/core/src/tracing/baggage.js b/packages/core/src/tracing/baggage.js index a0c609b350..a11fc315d1 100644 --- a/packages/core/src/tracing/baggage.js +++ b/packages/core/src/tracing/baggage.js @@ -52,15 +52,20 @@ function renderBaggageHeader(entries) { /** * @param {string | null} rawBaggage * @param {string[]} captureKeys - * @param {Record} tags + * @param {import('./cls').InstanaSpan} span */ -function applyCaptureTags(rawBaggage, captureKeys, tags) { +function applyCaptureTags(rawBaggage, captureKeys, span) { if (!rawBaggage || !captureKeys || captureKeys.length === 0) { return; } const parsed = parseBaggageHeader(rawBaggage); + if (!span.data.sdk) span.data.sdk = {}; + if (!span.data.sdk.custom) span.data.sdk.custom = {}; + if (!span.data.sdk.custom.tags) span.data.sdk.custom.tags = {}; + + const tags = span.data.sdk.custom.tags; for (let i = 0; i < captureKeys.length; i++) { const key = captureKeys[i]; if (parsed[key] !== undefined && tags[key] === undefined) { diff --git a/packages/core/src/tracing/cls.js b/packages/core/src/tracing/cls.js index 2cf607bfbf..ae030b0c69 100644 --- a/packages/core/src/tracing/cls.js +++ b/packages/core/src/tracing/cls.js @@ -379,10 +379,7 @@ function startSpan(spanAttributes = {}) { if (captureW3cBaggage.length > 0) { const rawBaggage = ns.get(baggageKey); if (rawBaggage) { - if (!span.data.sdk) span.data.sdk = {}; - if (!span.data.sdk.custom) span.data.sdk.custom = {}; - if (!span.data.sdk.custom.tags) span.data.sdk.custom.tags = {}; - baggageUtil.applyCaptureTags(rawBaggage, captureW3cBaggage, span.data.sdk.custom.tags); + baggageUtil.applyCaptureTags(rawBaggage, captureW3cBaggage, span); } } diff --git a/packages/core/test/tracing/baggage_capturing_test.js b/packages/core/test/tracing/baggage_capturing_test.js index b5e3c64abf..f480ebf3d2 100644 --- a/packages/core/test/tracing/baggage_capturing_test.js +++ b/packages/core/test/tracing/baggage_capturing_test.js @@ -127,41 +127,49 @@ describe('baggage capturing', () => { baggage = require('../../src/tracing/baggage'); }); + const makeSpan = (existingTags) => { + const span = { data: {} }; + if (existingTags) { + span.data.sdk = { custom: { tags: existingTags } }; + } + return span; + }; + it('captures only configured keys', () => { - const tags = {}; - baggage.applyCaptureTags('userId=alice,isPremium=true,region=eu', ['userId'], tags); - expect(tags).to.deep.equal({ userId: 'alice' }); + const span = makeSpan(); + baggage.applyCaptureTags('userId=alice,isPremium=true,region=eu', ['userId'], span); + expect(span.data.sdk.custom.tags).to.deep.equal({ userId: 'alice' }); }); it('does not capture keys not in the capture list', () => { - const tags = {}; - baggage.applyCaptureTags('userId=alice,region=eu', ['userId'], tags); - expect(tags).to.not.have.property('region'); + const span = makeSpan(); + baggage.applyCaptureTags('userId=alice,region=eu', ['userId'], span); + expect(span.data.sdk.custom.tags).to.not.have.property('region'); }); it('does not overwrite existing tag (existing annotation wins)', () => { - const tags = { userId: 'existing' }; - baggage.applyCaptureTags('userId=fromBaggage', ['userId'], tags); - expect(tags.userId).to.equal('existing'); + const span = makeSpan({ userId: 'existing' }); + baggage.applyCaptureTags('userId=fromBaggage', ['userId'], span); + expect(span.data.sdk.custom.tags.userId).to.equal('existing'); }); it('does nothing when captureKeys is empty', () => { - const tags = {}; - baggage.applyCaptureTags('userId=alice', [], tags); - expect(tags).to.deep.equal({}); + const span = makeSpan(); + baggage.applyCaptureTags('userId=alice', [], span); + expect(span.data).to.deep.equal({}); }); it('does nothing when rawBaggage is null', () => { - const tags = {}; - baggage.applyCaptureTags(null, ['userId'], tags); - expect(tags).to.deep.equal({}); + const span = makeSpan(); + baggage.applyCaptureTags(null, ['userId'], span); + expect(span.data).to.deep.equal({}); }); it('does not capture properties', () => { - const tags = {}; - baggage.applyCaptureTags('userId=alice;prop1=v', ['userId'], tags); - expect(tags).to.deep.equal({ userId: 'alice' }); - expect(tags).to.not.have.property('prop1'); + const span = makeSpan(); + baggage.applyCaptureTags('userId=alice;prop1=v', ['userId'], span); + expect(span.data.sdk.custom.tags).to.deep.equal({ userId: 'alice' }); + expect(span.data.sdk.custom.tags).to.not.have.property('prop1'); }); }); From c9635998522afd9aa9e0849a46c5d68235177c29 Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Thu, 17 Sep 2026 11:18:47 +0200 Subject: [PATCH 06/18] chore: added todo --- .../traces/mappers/instanaInstrumentationMappings.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js b/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js index 16dd965a76..6af1797b21 100644 --- a/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js +++ b/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js @@ -578,6 +578,7 @@ module.exports = { } } + // TODO: where to put const customTags = span.data?.sdk?.custom?.tags; if (customTags && typeof customTags === 'object') { const tagKeys = Object.keys(customTags); From 9726767310aaa376343599a113cd109978e7239e Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Thu, 17 Sep 2026 11:24:16 +0200 Subject: [PATCH 07/18] chore: lint --- packages/core/src/tracing/baggage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/tracing/baggage.js b/packages/core/src/tracing/baggage.js index a11fc315d1..1ac8be30e9 100644 --- a/packages/core/src/tracing/baggage.js +++ b/packages/core/src/tracing/baggage.js @@ -52,7 +52,7 @@ function renderBaggageHeader(entries) { /** * @param {string | null} rawBaggage * @param {string[]} captureKeys - * @param {import('./cls').InstanaSpan} span + * @param {{ data: { sdk?: { custom?: { tags?: Record } } } }} span */ function applyCaptureTags(rawBaggage, captureKeys, span) { if (!rawBaggage || !captureKeys || captureKeys.length === 0) { From 213d8ca1933caffdedc33f2bd3c2f160846ec7a5 Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Fri, 18 Sep 2026 10:41:06 +0200 Subject: [PATCH 08/18] chore: revert --- .../traces/mappers/instanaInstrumentationMappings.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js b/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js index 6af1797b21..209bec5a93 100644 --- a/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js +++ b/packages/core/src/otlpExporter/traces/mappers/instanaInstrumentationMappings.js @@ -578,18 +578,6 @@ module.exports = { } } - // TODO: where to put - const customTags = span.data?.sdk?.custom?.tags; - if (customTags && typeof customTags === 'object') { - const tagKeys = Object.keys(customTags); - for (let i = 0; i < tagKeys.length; i++) { - const k = tagKeys[i]; - if (customTags[k] !== null && customTags[k] !== undefined) { - attributes.push({ key: k, value: formatOTLPValue(customTags[k]) }); - } - } - } - return attributes; }, From 098fd4a5a8e05c3743fc2a787d0a0cd8fadc3400 Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Fri, 18 Sep 2026 10:59:52 +0200 Subject: [PATCH 09/18] test: more tests --- .../misc/w3c_trace_context/test_base.js | 106 +++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/packages/collector/test/integration/misc/w3c_trace_context/test_base.js b/packages/collector/test/integration/misc/w3c_trace_context/test_base.js index 94e0ce5cf4..653c073706 100644 --- a/packages/collector/test/integration/misc/w3c_trace_context/test_base.js +++ b/packages/collector/test/integration/misc/w3c_trace_context/test_base.js @@ -1151,9 +1151,108 @@ module.exports = function (name, version, isLatest, mode) { })); }); }); + + describe('W3C baggage capturing', () => { + let baggageAppControls; + + before(async () => { + baggageAppControls = new ProcessControls({ + dirname: __dirname, + useGlobalAgent: true, + http2: isHTTP2, + env: { + APM_VENDOR: 'instana', + DOWNSTREAM_PORT: otherVendorAppPort, + APP_USES_HTTP2: isHTTP2, + INSTANA_TRACING_CAPTURE_W3C_BAGGAGE: 'userId,requestId' + } + }); + await baggageAppControls.startAndWaitForAgentConnection(); + }); + + after(async () => { + await baggageAppControls.stop(); + }); + + it('should NOT capture baggage keys when INSTANA_TRACING_CAPTURE_W3C_BAGGAGE is not configured', () => + startRequest({ + app: instanaAppControls, + depth: 1, + withSpecHeaders: 'valid-sampled-with-random-trace-id', + withBaggageHeader: 'userId=alice,requestId=req-42' + }).then(() => + retryUntilSpansMatch(agentControls, spans => { + const entry = verifyHttpRootEntry({ spans, url: '/start', instanaAppControls }); + expect(entry.data.sdk).to.not.exist; + }) + )); + + it('should capture configured baggage keys into span tags on the HTTP entry span', () => + startRequest({ + app: baggageAppControls, + depth: 1, + withSpecHeaders: 'valid-sampled-with-random-trace-id', + withBaggageHeader: 'userId=alice,requestId=req-42,ignored=drop-me' + }).then(() => + retryUntilSpansMatch(agentControls, spans => { + const entry = expectExactlyOneMatching(spans, [ + span => expect(span.n).to.equal('node.http.server'), + span => expect(span.k).to.equal(constants.ENTRY), + span => expect(span.data.http.url).to.equal('/start'), + span => expect(span.data.http.host).to.equal(`localhost:${baggageAppControls.getPort()}`) + ]); + expect(entry.data.sdk.custom.tags.userId).to.equal('alice'); + expect(entry.data.sdk.custom.tags.requestId).to.equal('req-42'); + expect(entry.data.sdk.custom.tags.ignored).to.not.exist; + }) + )); + + it('should not capture baggage keys when the baggage header is absent', () => + startRequest({ + app: baggageAppControls, + depth: 1, + withSpecHeaders: 'valid-sampled-with-random-trace-id' + }).then(() => + retryUntilSpansMatch(agentControls, spans => { + const entry = expectExactlyOneMatching(spans, [ + span => expect(span.n).to.equal('node.http.server'), + span => expect(span.k).to.equal(constants.ENTRY), + span => expect(span.data.http.url).to.equal('/start'), + span => expect(span.data.http.host).to.equal(`localhost:${baggageAppControls.getPort()}`) + ]); + expect(entry.data.sdk).to.not.exist; + }) + )); + + it('should capture percent-encoded baggage values correctly', () => + startRequest({ + app: baggageAppControls, + depth: 1, + withSpecHeaders: 'valid-sampled-with-random-trace-id', + withBaggageHeader: `userId=${encodeURIComponent('alice smith')},requestId=req-99` + }).then(() => + retryUntilSpansMatch(agentControls, spans => { + const entry = expectExactlyOneMatching(spans, [ + span => expect(span.n).to.equal('node.http.server'), + span => expect(span.k).to.equal(constants.ENTRY), + span => expect(span.data.http.url).to.equal('/start'), + span => expect(span.data.http.host).to.equal(`localhost:${baggageAppControls.getPort()}`) + ]); + expect(entry.data.sdk.custom.tags.userId).to.equal('alice smith'); + expect(entry.data.sdk.custom.tags.requestId).to.equal('req-99'); + }) + )); + }); }; -function startRequest({ app, depth = 2, withSpecHeaders = null, otherMode = 'participate', withInstanaHeaders }) { +function startRequest({ + app, + depth = 2, + withSpecHeaders = null, + otherMode = 'participate', + withInstanaHeaders, + withBaggageHeader = null +}) { const request = { path: `/start?depth=${depth}&otherMode=${otherMode}` }; @@ -1196,6 +1295,11 @@ function startRequest({ app, depth = 2, withSpecHeaders = null, otherMode = 'par throw new Error(`Invalid withSpecHeaders value: ${withSpecHeaders}.`); } + if (withBaggageHeader) { + request.headers = request.headers || {}; + request.headers['baggage'] = withBaggageHeader; + } + if (withInstanaHeaders === 'trace-in-progress') { request.headers = request.headers || {}; request.headers['X-INSTANA-T'] = upstreamInstanaTraceId; From 7c5a6736a5d676d3e5f1aa1dc9348930af515959 Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Fri, 18 Sep 2026 11:13:41 +0200 Subject: [PATCH 10/18] test: more tests --- .../integration/misc/w3c_trace_context/app.js | 3 +- .../misc/w3c_trace_context/test_base.js | 139 ++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/packages/collector/test/integration/misc/w3c_trace_context/app.js b/packages/collector/test/integration/misc/w3c_trace_context/app.js index f52b684c1b..f24c0a4014 100644 --- a/packages/collector/test/integration/misc/w3c_trace_context/app.js +++ b/packages/collector/test/integration/misc/w3c_trace_context/app.js @@ -195,7 +195,8 @@ function handleRequest(incomingHeaders, method, url, resOrStream) { w3cTraceContext: { receivedHeaders: { traceparent: incomingHeaders.traceparent, - tracestate: incomingHeaders.tracestate + tracestate: incomingHeaders.tracestate, + baggage: incomingHeaders.baggage } } }; diff --git a/packages/collector/test/integration/misc/w3c_trace_context/test_base.js b/packages/collector/test/integration/misc/w3c_trace_context/test_base.js index 653c073706..3a47f6f05d 100644 --- a/packages/collector/test/integration/misc/w3c_trace_context/test_base.js +++ b/packages/collector/test/integration/misc/w3c_trace_context/test_base.js @@ -1207,6 +1207,24 @@ module.exports = function (name, version, isLatest, mode) { }) )); + it('should capture baggage keys on child (exit) spans within the same process', () => + startRequest({ + app: baggageAppControls, + depth: 1, + withSpecHeaders: 'valid-sampled-with-random-trace-id', + withBaggageHeader: 'userId=alice,requestId=req-42' + }).then(() => + retryUntilSpansMatch(agentControls, spans => { + const exit = expectExactlyOneMatching(spans, [ + span => expect(span.n).to.equal('node.http.client'), + span => expect(span.k).to.equal(constants.EXIT), + span => expect(span.data.http.host).to.include(`${otherVendorAppPort}`) + ]); + expect(exit.data.sdk.custom.tags.userId).to.equal('alice'); + expect(exit.data.sdk.custom.tags.requestId).to.equal('req-42'); + }) + )); + it('should not capture baggage keys when the baggage header is absent', () => startRequest({ app: baggageAppControls, @@ -1242,6 +1260,127 @@ module.exports = function (name, version, isLatest, mode) { expect(entry.data.sdk.custom.tags.requestId).to.equal('req-99'); }) )); + + it('should propagate the baggage header as-is to downstream services', () => + startRequest({ + app: instanaAppControls, + depth: 1, + withSpecHeaders: 'valid-sampled-with-random-trace-id', + withBaggageHeader: 'userId=alice;meta=1,requestId=req-42' + }).then(response => { + response = response && response.body ? JSON.parse(response.body) : response; + expect(response.w3cTraceContext.receivedHeaders.baggage).to.equal('userId=alice;meta=1,requestId=req-42'); + })); + + it('should NOT propagate the baggage header when INSTANA_TRACING_DISABLE_W3C_BAGGAGE is set', () => { + const disabledBaggageControls = new ProcessControls({ + dirname: __dirname, + useGlobalAgent: true, + http2: isHTTP2, + env: { + APM_VENDOR: 'instana', + DOWNSTREAM_PORT: otherVendorAppPort, + APP_USES_HTTP2: isHTTP2, + INSTANA_TRACING_DISABLE_W3C_BAGGAGE: 'true' + } + }); + + return disabledBaggageControls + .startAndWaitForAgentConnection() + .then(() => + startRequest({ + app: disabledBaggageControls, + depth: 1, + withSpecHeaders: 'valid-sampled-with-random-trace-id', + withBaggageHeader: 'userId=alice' + }) + ) + .then(response => { + response = response && response.body ? JSON.parse(response.body) : response; + expect(response.w3cTraceContext.receivedHeaders.baggage).to.not.exist; + }) + .finally(() => disabledBaggageControls.stop()); + }); + }); + + describe('W3C baggage capturing via agent yaml', () => { + const { AgentStubControls } = require('@_local/collector/test/apps/agentStubControls'); + const agentStubControls = new AgentStubControls(); + let agentYamlAppControls; + + before(async () => { + await agentStubControls.startAgent({ + w3cDisableConfig: { 'capture-w3c-baggage': 'userId,requestId' } + }); + agentYamlAppControls = new ProcessControls({ + dirname: __dirname, + agentControls: agentStubControls, + http2: isHTTP2, + env: { + APM_VENDOR: 'instana', + DOWNSTREAM_PORT: otherVendorAppPort, + APP_USES_HTTP2: isHTTP2 + } + }); + await agentYamlAppControls.startAndWaitForAgentConnection(); + }); + + after(async () => { + await agentYamlAppControls.stop(); + await agentStubControls.stopAgent(); + }); + + it('should capture configured keys from agent yaml capture-w3c-baggage', () => + startRequest({ + app: agentYamlAppControls, + depth: 1, + withSpecHeaders: 'valid-sampled-with-random-trace-id', + withBaggageHeader: 'userId=bob,requestId=req-7,other=ignore' + }).then(() => + retryUntilSpansMatch(agentStubControls, spans => { + const entry = expectExactlyOneMatching(spans, [ + span => expect(span.n).to.equal('node.http.server'), + span => expect(span.k).to.equal(constants.ENTRY), + span => expect(span.data.http.url).to.equal('/start'), + span => expect(span.data.http.host).to.equal(`localhost:${agentYamlAppControls.getPort()}`) + ]); + expect(entry.data.sdk.custom.tags.userId).to.equal('bob'); + expect(entry.data.sdk.custom.tags.requestId).to.equal('req-7'); + expect(entry.data.sdk.custom.tags.other).to.not.exist; + }) + )); + + it('should disable baggage propagation via agent yaml disable-w3c-baggage', async () => { + const disableAgentStub = new AgentStubControls(); + await disableAgentStub.startAgent({ + w3cDisableConfig: { 'disable-w3c-baggage': true } + }); + const disableAgentApp = new ProcessControls({ + dirname: __dirname, + agentControls: disableAgentStub, + http2: isHTTP2, + env: { + APM_VENDOR: 'instana', + DOWNSTREAM_PORT: otherVendorAppPort, + APP_USES_HTTP2: isHTTP2 + } + }); + await disableAgentApp.startAndWaitForAgentConnection(); + + try { + const response = await startRequest({ + app: disableAgentApp, + depth: 1, + withSpecHeaders: 'valid-sampled-with-random-trace-id', + withBaggageHeader: 'userId=alice' + }); + const parsed = response && response.body ? JSON.parse(response.body) : response; + expect(parsed.w3cTraceContext.receivedHeaders.baggage).to.not.exist; + } finally { + await disableAgentApp.stop(); + await disableAgentStub.stopAgent(); + } + }); }); }; From 77c7a15e4a00c738a6ccadc4f619d232435973ec Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Fri, 18 Sep 2026 11:23:53 +0200 Subject: [PATCH 11/18] fix: lint --- .../integration/misc/w3c_trace_context/test_base.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/collector/test/integration/misc/w3c_trace_context/test_base.js b/packages/collector/test/integration/misc/w3c_trace_context/test_base.js index 3a47f6f05d..14222063e1 100644 --- a/packages/collector/test/integration/misc/w3c_trace_context/test_base.js +++ b/packages/collector/test/integration/misc/w3c_trace_context/test_base.js @@ -1150,9 +1150,8 @@ module.exports = function (name, version, isLatest, mode) { }); })); }); - }); - describe('W3C baggage capturing', () => { + describe('W3C baggage capturing', () => { let baggageAppControls; before(async () => { @@ -1301,9 +1300,9 @@ module.exports = function (name, version, isLatest, mode) { }) .finally(() => disabledBaggageControls.stop()); }); - }); + }); - describe('W3C baggage capturing via agent yaml', () => { + describe('W3C baggage capturing via agent yaml', () => { const { AgentStubControls } = require('@_local/collector/test/apps/agentStubControls'); const agentStubControls = new AgentStubControls(); let agentYamlAppControls; @@ -1381,6 +1380,7 @@ module.exports = function (name, version, isLatest, mode) { await disableAgentStub.stopAgent(); } }); + }); }); }; @@ -1436,7 +1436,7 @@ function startRequest({ if (withBaggageHeader) { request.headers = request.headers || {}; - request.headers['baggage'] = withBaggageHeader; + request.headers.baggage = withBaggageHeader; } if (withInstanaHeaders === 'trace-in-progress') { From 2a43538a08a6935f1dfe29f4312d44ac2a2bae01 Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Fri, 18 Sep 2026 12:04:56 +0200 Subject: [PATCH 12/18] chore: cleanup --- .../instrumentation/protocols/http2Client.js | 8 ++------ .../instrumentation/protocols/httpClient.js | 20 +++++-------------- .../instrumentation/protocols/nativeFetch.js | 8 ++------ packages/core/src/tracing/tracingHeaders.js | 4 ++-- 4 files changed, 11 insertions(+), 29 deletions(-) diff --git a/packages/core/src/tracing/instrumentation/protocols/http2Client.js b/packages/core/src/tracing/instrumentation/protocols/http2Client.js index 6ec2b35a81..add4d184c5 100644 --- a/packages/core/src/tracing/instrumentation/protocols/http2Client.js +++ b/packages/core/src/tracing/instrumentation/protocols/http2Client.js @@ -152,9 +152,7 @@ function addTraceLevelHeader(headers, level, w3cTraceContext) { return; } headers[constants.traceLevelHeaderName] = level; - tracingHeaders.addW3cHeaders((name, value) => { - headers[name] = value; - }, w3cTraceContext, cls); + tracingHeaders.addW3cHeaders((k, v) => { headers[k] = v; }, w3cTraceContext, cls); } function addHeaders(headers, span, w3cTraceContext) { @@ -169,7 +167,5 @@ function addHeaders(headers, span, w3cTraceContext) { headers[constants.spanIdHeaderName] = span.s; headers[constants.traceIdHeaderName] = span.t; headers[constants.traceLevelHeaderName] = '1'; - tracingHeaders.addW3cHeaders((name, value) => { - headers[name] = value; - }, w3cTraceContext, cls); + tracingHeaders.addW3cHeaders((k, v) => { headers[k] = v; }, w3cTraceContext, cls); } diff --git a/packages/core/src/tracing/instrumentation/protocols/httpClient.js b/packages/core/src/tracing/instrumentation/protocols/httpClient.js index ec7d67ce14..24b8e41a65 100644 --- a/packages/core/src/tracing/instrumentation/protocols/httpClient.js +++ b/packages/core/src/tracing/instrumentation/protocols/httpClient.js @@ -192,9 +192,7 @@ function instrument(coreModule, forceHttps) { removeInstanaHeadersFromOpts(options); if (skipTracingResult.suppressed && !traceLevelHeaderHasBeenAdded) { clientRequest.setHeader(constants.traceLevelHeaderName, '0'); - tracingHeaders.addW3cHeaders((name, value) => { - clientRequest.setHeader(name, value); - }, w3cTraceContext); + tracingHeaders.addW3cHeaders((k, v) => clientRequest.setHeader(k, v), w3cTraceContext, cls); } return clientRequest; @@ -397,9 +395,7 @@ function tryToAddHeadersToOpts(options, span, w3cTraceContext) { options.headers[constants.spanIdHeaderName] = span.s; options.headers[constants.traceIdHeaderName] = span.t; options.headers[constants.traceLevelHeaderName] = '1'; - tracingHeaders.addW3cHeaders((name, value) => { - options.headers[name] = value; - }, w3cTraceContext, cls); + tracingHeaders.addW3cHeaders((k, v) => { options.headers[k] = v; }, w3cTraceContext, cls); return true; } @@ -409,9 +405,7 @@ function tryToAddHeadersToOpts(options, span, w3cTraceContext) { function tryToAddTraceLevelAddHeaderToOpts(options, level, w3cTraceContext) { if (hasHeadersOption(options)) { options.headers[constants.traceLevelHeaderName] = level; - tracingHeaders.addW3cHeaders((name, value) => { - options.headers[name] = value; - }, w3cTraceContext, cls); + tracingHeaders.addW3cHeaders((k, v) => { options.headers[k] = v; }, w3cTraceContext, cls); return true; } return false; @@ -438,18 +432,14 @@ function setHeadersOnRequest(clientRequest, span, w3cTraceContext) { if (span.shouldSuppressDownstream) { // Suppress trace propagation to downstream services. clientRequest.setHeader(constants.traceLevelHeaderName, '0'); - tracingHeaders.addW3cHeaders((name, value) => { - clientRequest.setHeader(name, value); - }, w3cTraceContext, cls); + tracingHeaders.addW3cHeaders((k, v) => clientRequest.setHeader(k, v), w3cTraceContext, cls); return; } clientRequest.setHeader(constants.spanIdHeaderName, span.s); clientRequest.setHeader(constants.traceIdHeaderName, span.t); clientRequest.setHeader(constants.traceLevelHeaderName, '1'); - tracingHeaders.addW3cHeaders((name, value) => { - clientRequest.setHeader(name, value); - }, w3cTraceContext, cls); + tracingHeaders.addW3cHeaders((k, v) => clientRequest.setHeader(k, v), w3cTraceContext, cls); } function captureRequestHeaders(options, clientRequest, response) { diff --git a/packages/core/src/tracing/instrumentation/protocols/nativeFetch.js b/packages/core/src/tracing/instrumentation/protocols/nativeFetch.js index 1590fb93c3..7a78e2439c 100644 --- a/packages/core/src/tracing/instrumentation/protocols/nativeFetch.js +++ b/packages/core/src/tracing/instrumentation/protocols/nativeFetch.js @@ -207,9 +207,7 @@ function injectTraceCorrelationHeaders(originalArgs, span, w3cTraceContext) { [constants.spanIdHeaderName]: span.s, [constants.traceLevelHeaderName]: '1' }; - tracingHeaders.addW3cHeaders((name, value) => { - headersToAdd[name] = value; - }, w3cTraceContext, cls); + tracingHeaders.addW3cHeaders((k, v) => { headersToAdd[k] = v; }, w3cTraceContext, cls); injectHeaders(originalArgs, headersToAdd); } @@ -217,9 +215,7 @@ function injectSuppressionHeader(originalArgs, w3cTraceContext) { const headersToAdd = { [constants.traceLevelHeaderName]: '0' }; - tracingHeaders.addW3cHeaders((name, value) => { - headersToAdd[name] = value; - }, w3cTraceContext, cls); + tracingHeaders.addW3cHeaders((k, v) => { headersToAdd[k] = v; }, w3cTraceContext, cls); injectHeaders(originalArgs, headersToAdd); } diff --git a/packages/core/src/tracing/tracingHeaders.js b/packages/core/src/tracing/tracingHeaders.js index e0fc3ac740..e91610c2c2 100644 --- a/packages/core/src/tracing/tracingHeaders.js +++ b/packages/core/src/tracing/tracingHeaders.js @@ -477,7 +477,7 @@ exports.setSpanAttributes = function (span, tracingHeaders) { * Writes traceparent, tracestate and baggage headers using the provided setter function. * @param {(key: string, value: string) => void} set * @param {import('./w3c_trace_context/W3cTraceContext')} w3cTraceContext - * @param {{ getBaggage: () => string | null }} [cls] + * @param {{ getBaggage: () => string | null }} cls */ exports.addW3cHeaders = function addW3cHeaders(set, w3cTraceContext, cls) { if (disableW3cPropagation) { @@ -489,7 +489,7 @@ exports.addW3cHeaders = function addW3cHeaders(set, w3cTraceContext, cls) { set(constants.w3cTraceState, w3cTraceContext.renderTraceState()); } } - if (!disableW3cBaggage && cls && typeof cls.getBaggage === 'function') { + if (!disableW3cBaggage) { const baggage = cls.getBaggage(); if (baggage) { set(constants.w3cBaggage, baggage); From 69cbf44643695114ef42844796ce831596252e9f Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Fri, 18 Sep 2026 12:15:52 +0200 Subject: [PATCH 13/18] chore: clean --- .../instrumentation/protocols/http2Server.js | 6 ++--- .../instrumentation/protocols/httpServer.js | 6 ++--- packages/core/src/tracing/tracingHeaders.js | 16 +++++++------- .../core/test/tracing/tracingHeaders_test.js | 22 +++++++++---------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/core/src/tracing/instrumentation/protocols/http2Server.js b/packages/core/src/tracing/instrumentation/protocols/http2Server.js index c4efc23e8c..caedf3fd25 100644 --- a/packages/core/src/tracing/instrumentation/protocols/http2Server.js +++ b/packages/core/src/tracing/instrumentation/protocols/http2Server.js @@ -77,7 +77,7 @@ function shimEmit(realEmit) { } const incomingHeaders = tracingHeaders.fromHeaders(headers); - const w3cTraceContext = incomingHeaders.w3cTraceContext; + const { w3cTraceContext, w3cBaggage } = incomingHeaders; if (typeof incomingHeaders.level === 'string' && incomingHeaders.level.indexOf('0') === 0) { cls.setTracingLevel('0'); @@ -93,8 +93,8 @@ function shimEmit(realEmit) { cls.setW3cTraceContext(w3cTraceContext); } - if (incomingHeaders.baggage != null) { - cls.setBaggage(incomingHeaders.baggage); + if (w3cBaggage != null) { + cls.setBaggage(w3cBaggage); } if (cls.tracingSuppressed()) { diff --git a/packages/core/src/tracing/instrumentation/protocols/httpServer.js b/packages/core/src/tracing/instrumentation/protocols/httpServer.js index 10b1c53baf..3e63cc1b01 100644 --- a/packages/core/src/tracing/instrumentation/protocols/httpServer.js +++ b/packages/core/src/tracing/instrumentation/protocols/httpServer.js @@ -59,7 +59,7 @@ function shimEmit(realEmit) { cls.ns.bindEmitter(res); } const incomingHeaders = tracingHeaders.fromHttpRequest(req); - const w3cTraceContext = incomingHeaders.w3cTraceContext; + const { w3cTraceContext, w3cBaggage } = incomingHeaders; if (typeof incomingHeaders.level === 'string' && incomingHeaders.level.indexOf('0') === 0) { cls.setTracingLevel('0'); @@ -75,8 +75,8 @@ function shimEmit(realEmit) { cls.setW3cTraceContext(w3cTraceContext); } - if (incomingHeaders.baggage != null) { - cls.setBaggage(incomingHeaders.baggage); + if (w3cBaggage != null) { + cls.setBaggage(w3cBaggage); } if (cls.tracingSuppressed()) { diff --git a/packages/core/src/tracing/tracingHeaders.js b/packages/core/src/tracing/tracingHeaders.js index e91610c2c2..05f3df73e4 100644 --- a/packages/core/src/tracing/tracingHeaders.js +++ b/packages/core/src/tracing/tracingHeaders.js @@ -62,7 +62,7 @@ exports.activate = function (config) { * - the tracing level, either '1' (tracing) or '0' (suppressing/not creating spans) * - progated downstream as the first component of X-INSTANA-L * - propagted downstream as the sampled flag in traceparent - * @property {string} [baggage] + * @property {string} [w3cBaggage] * - the raw W3C baggage header value read from the incoming request * - will be propagated downstream as-is (including properties) * - null if baggage support is disabled or the header violates size/count limits @@ -134,7 +134,7 @@ exports.fromHeaders = function fromHeaders(headers) { let correlationId = levelAndCorrelation.correlationId; const synthetic = readSyntheticMarker(headers); let w3cTraceContext = readW3cTraceContext(headers); - const baggage = readBaggage(headers); + const w3cBaggage = readBaggage(headers); if (isSuppressed(level)) { // Ignore X-INSTANA-T/-S if X-INSTANA-L: 0 is also present. @@ -163,7 +163,7 @@ exports.fromHeaders = function fromHeaders(headers) { correlationId, synthetic, w3cTraceContext, - baggage + w3cBaggage }; return exports.limitTraceId(result); } else if (xInstanaT && xInstanaS) { @@ -182,7 +182,7 @@ exports.fromHeaders = function fromHeaders(headers) { /** @type {string} */ (xInstanaS), !isSuppressed(level) ), - baggage + w3cBaggage }); } else if (w3cTraceContext && !disableW3cCorrelation) { // There are no X-INSTANA- headers, but there are W3C trace context headers. As of 2021-02, we use the IDs from @@ -215,7 +215,7 @@ exports.fromHeaders = function fromHeaders(headers) { synthetic, w3cTraceContext, instanaAncestor, - baggage + w3cBaggage }); } else if (w3cTraceContext) { // There are no X-INSTANA- headers, but there are W3C trace context headers. But picking up the trace context from @@ -243,7 +243,7 @@ exports.fromHeaders = function fromHeaders(headers) { correlationId, synthetic, w3cTraceContext, - baggage + w3cBaggage }); } else { // Neither X-INSTANA- headers nor W3C trace context headers are present. @@ -258,7 +258,7 @@ exports.fromHeaders = function fromHeaders(headers) { level, synthetic, w3cTraceContext: w3c.createEmptyUnsampled(generateRandomTraceId(), generateRandomSpanId()), - baggage + w3cBaggage }); } else { // Neither X-INSTANA- headers nor W3C trace context headers are present and tracing is not suppressed @@ -278,7 +278,7 @@ exports.fromHeaders = function fromHeaders(headers) { correlationId, synthetic, w3cTraceContext, - baggage + w3cBaggage }); } } diff --git a/packages/core/test/tracing/tracingHeaders_test.js b/packages/core/test/tracing/tracingHeaders_test.js index a53edfd6a1..15b88e600f 100644 --- a/packages/core/test/tracing/tracingHeaders_test.js +++ b/packages/core/test/tracing/tracingHeaders_test.js @@ -609,14 +609,14 @@ describe('tracing/headers', () => { const context = tracingHeaders.fromHttpRequest({ headers: { baggage: 'userId=alice,isPremium=true' } }); - expect(context.baggage).to.equal('userId=alice,isPremium=true'); + expect(context.w3cBaggage).to.equal('userId=alice,isPremium=true'); }); it('should read baggage header case-insensitively', () => { const context = tracingHeaders.fromHttpRequest({ headers: { Baggage: 'userId=alice' } }); - expect(context.baggage).to.equal('userId=alice'); + expect(context.w3cBaggage).to.equal('userId=alice'); }); it('should read baggage alongside X-INSTANA headers', () => { @@ -627,7 +627,7 @@ describe('tracing/headers', () => { baggage: 'userId=alice' } }); - expect(context.baggage).to.equal('userId=alice'); + expect(context.w3cBaggage).to.equal('userId=alice'); }); it('should read baggage alongside traceparent', () => { @@ -637,7 +637,7 @@ describe('tracing/headers', () => { baggage: 'userId=alice' } }); - expect(context.baggage).to.equal('userId=alice'); + expect(context.w3cBaggage).to.equal('userId=alice'); }); it('should preserve baggage properties (semicolon-separated metadata)', () => { @@ -645,12 +645,12 @@ describe('tracing/headers', () => { const context = tracingHeaders.fromHttpRequest({ headers: { baggage: raw } }); - expect(context.baggage).to.equal(raw); + expect(context.w3cBaggage).to.equal(raw); }); it('should return null baggage when no baggage header is present', () => { const context = tracingHeaders.fromHttpRequest({ headers: {} }); - expect(context.baggage).to.be.null; + expect(context.w3cBaggage).to.be.null; }); it('should drop baggage when it exceeds 8192 bytes', () => { @@ -658,7 +658,7 @@ describe('tracing/headers', () => { const context = tracingHeaders.fromHttpRequest({ headers: { baggage: longValue } }); - expect(context.baggage).to.be.null; + expect(context.w3cBaggage).to.be.null; }); it('should accept baggage at exactly 8192 bytes', () => { @@ -666,7 +666,7 @@ describe('tracing/headers', () => { const context = tracingHeaders.fromHttpRequest({ headers: { baggage: value } }); - expect(context.baggage).to.equal(value); + expect(context.w3cBaggage).to.equal(value); }); it('should drop baggage when it contains more than 64 list-members', () => { @@ -674,7 +674,7 @@ describe('tracing/headers', () => { const context = tracingHeaders.fromHttpRequest({ headers: { baggage: pairs } }); - expect(context.baggage).to.be.null; + expect(context.w3cBaggage).to.be.null; }); it('should accept baggage with exactly 64 list-members', () => { @@ -682,7 +682,7 @@ describe('tracing/headers', () => { const context = tracingHeaders.fromHttpRequest({ headers: { baggage: pairs } }); - expect(context.baggage).to.equal(pairs); + expect(context.w3cBaggage).to.equal(pairs); }); describe('when disableW3cBaggage is true', () => { @@ -705,7 +705,7 @@ describe('tracing/headers', () => { const context = tracingHeaders.fromHttpRequest({ headers: { baggage: 'userId=alice' } }); - expect(context.baggage).to.be.null; + expect(context.w3cBaggage).to.be.null; }); }); }); From 6a2aea68e06bb7e934312606372f11329c7e8a6a Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Fri, 18 Sep 2026 16:14:26 +0200 Subject: [PATCH 14/18] test: more tests --- .../integration/misc/w3c_trace_context/app.js | 8 ++++-- .../misc/w3c_trace_context/test_base.js | 27 ++++++++++++++++++- .../test/tracing/baggage_capturing_test.js | 17 ++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/collector/test/integration/misc/w3c_trace_context/app.js b/packages/collector/test/integration/misc/w3c_trace_context/app.js index f24c0a4014..5d52da8c57 100644 --- a/packages/collector/test/integration/misc/w3c_trace_context/app.js +++ b/packages/collector/test/integration/misc/w3c_trace_context/app.js @@ -37,8 +37,9 @@ if (vendor === 'instana') { } let cls; +let instana; if (isInstana()) { - require('@instana/collector')(); + instana = require('@instana/collector')(); cls = require('@instana/core/src/tracing/cls'); } @@ -164,10 +165,13 @@ function handleRequest(incomingHeaders, method, url, resOrStream) { return endWithStatus(method, url, resOrStream, 405); } return endWithStatus(method, url, resOrStream, 200); - } else if (pathname === '/start' || pathname === '/continue') { + } else if (pathname === '/start' || pathname === '/continue' || pathname === '/start-with-baggage') { if (method !== 'GET') { return endWithStatus(method, url, resOrStream, 405); } + if (pathname === '/start-with-baggage' && isInstana()) { + instana.sdk.async.setBaggage('customSdkKey', 'customSdkValue'); + } downstreamPath = depth > 1 ? 'continue' : 'end'; const requestOptions = { method, diff --git a/packages/collector/test/integration/misc/w3c_trace_context/test_base.js b/packages/collector/test/integration/misc/w3c_trace_context/test_base.js index 14222063e1..bee4fe8060 100644 --- a/packages/collector/test/integration/misc/w3c_trace_context/test_base.js +++ b/packages/collector/test/integration/misc/w3c_trace_context/test_base.js @@ -1271,6 +1271,30 @@ module.exports = function (name, version, isLatest, mode) { expect(response.w3cTraceContext.receivedHeaders.baggage).to.equal('userId=alice;meta=1,requestId=req-42'); })); + it('should propagate baggage set via instana.sdk.setBaggage to downstream services', () => + startRequest({ + app: instanaAppControls, + startPath: '/start-with-baggage', + depth: 1, + withSpecHeaders: 'valid-sampled-with-random-trace-id' + }).then(response => { + response = response && response.body ? JSON.parse(response.body) : response; + expect(response.w3cTraceContext.receivedHeaders.baggage).to.equal('customSdkKey=customSdkValue'); + })); + + it('should append baggage set via instana.sdk.setBaggage to incoming baggage headers', () => + startRequest({ + app: instanaAppControls, + startPath: '/start-with-baggage', + depth: 1, + withSpecHeaders: 'valid-sampled-with-random-trace-id', + withBaggageHeader: 'incomingKey=incomingVal' + }).then(response => { + response = response && response.body ? JSON.parse(response.body) : response; + expect(response.w3cTraceContext.receivedHeaders.baggage).to.include('incomingKey=incomingVal'); + expect(response.w3cTraceContext.receivedHeaders.baggage).to.include('customSdkKey=customSdkValue'); + })); + it('should NOT propagate the baggage header when INSTANA_TRACING_DISABLE_W3C_BAGGAGE is set', () => { const disabledBaggageControls = new ProcessControls({ dirname: __dirname, @@ -1386,6 +1410,7 @@ module.exports = function (name, version, isLatest, mode) { function startRequest({ app, + startPath = '/start', depth = 2, withSpecHeaders = null, otherMode = 'participate', @@ -1393,7 +1418,7 @@ function startRequest({ withBaggageHeader = null }) { const request = { - path: `/start?depth=${depth}&otherMode=${otherMode}` + path: `${startPath}?depth=${depth}&otherMode=${otherMode}` }; if (withSpecHeaders === 'valid-sampled-with-random-trace-id') { request.headers = { diff --git a/packages/core/test/tracing/baggage_capturing_test.js b/packages/core/test/tracing/baggage_capturing_test.js index f480ebf3d2..4864833b45 100644 --- a/packages/core/test/tracing/baggage_capturing_test.js +++ b/packages/core/test/tracing/baggage_capturing_test.js @@ -246,6 +246,23 @@ describe('baggage capturing', () => { sdk = generated; }); + it('is available on callback, promise, and async sdk exports', () => { + const sdkExports = require('../../src/tracing/sdk'); + sdkExports.init({ logger: createFakeLogger() }, cls); + sdkExports.activate(); + + cls.ns.run(() => { + sdkExports.callback.setBaggage('cbKey', 'cbVal'); + expect(cls.getBaggage()).to.include('cbKey=cbVal'); + + sdkExports.promise.setBaggage('promiseKey', 'promiseVal'); + expect(cls.getBaggage()).to.include('promiseKey=promiseVal'); + + sdkExports.async.setBaggage('asyncKey', 'asyncVal'); + expect(cls.getBaggage()).to.include('asyncKey=asyncVal'); + }); + }); + it('sets a new key in the baggage header stored in CLS', () => { cls.ns.run(() => { cls.setBaggage('existingKey=existingVal'); From 6c31dc64f20c3235d88717f4d6b9ad6a98ffeead Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Mon, 21 Sep 2026 14:54:03 +0200 Subject: [PATCH 15/18] chore: fix --- packages/core/src/tracing/cls.js | 8 ++++++++ packages/core/src/tracing/index.js | 1 + 2 files changed, 9 insertions(+) diff --git a/packages/core/src/tracing/cls.js b/packages/core/src/tracing/cls.js index ae030b0c69..b98b5262fc 100644 --- a/packages/core/src/tracing/cls.js +++ b/packages/core/src/tracing/cls.js @@ -62,6 +62,13 @@ function init(config, _processIdentityProvider) { captureW3cBaggage = config?.tracing?.captureW3cBaggage || []; } +/** + * @param {import('../config').InstanaConfig} config + */ +function activate(config) { + captureW3cBaggage = config?.tracing?.captureW3cBaggage || []; +} + class InstanaSpan { /** * @param {string} name @@ -762,6 +769,7 @@ module.exports = { baggageKey, ns, init, + activate, startSpan, putPseudoSpan, getCurrentEntrySpan, diff --git a/packages/core/src/tracing/index.js b/packages/core/src/tracing/index.js index 2df20e3f7f..9cdd8c9c40 100644 --- a/packages/core/src/tracing/index.js +++ b/packages/core/src/tracing/index.js @@ -265,6 +265,7 @@ function initInstanaInstrumentations(_config) { exports.activate = function activate(_config = config) { if (tracingEnabled && !tracingActivated) { tracingActivated = true; + cls.activate(_config); coreUtil.activate(_config); tracingUtil.activate(_config); spanBuffer.activate(_config); From 0915e2fed6d92bced362e10c8f1d5349e83c6b74 Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Mon, 21 Sep 2026 15:37:32 +0200 Subject: [PATCH 16/18] test: fixing --- .../test/integration/misc/w3c_trace_context/test_base.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/collector/test/integration/misc/w3c_trace_context/test_base.js b/packages/collector/test/integration/misc/w3c_trace_context/test_base.js index bee4fe8060..bd5afc93dc 100644 --- a/packages/collector/test/integration/misc/w3c_trace_context/test_base.js +++ b/packages/collector/test/integration/misc/w3c_trace_context/test_base.js @@ -1181,7 +1181,12 @@ module.exports = function (name, version, isLatest, mode) { withBaggageHeader: 'userId=alice,requestId=req-42' }).then(() => retryUntilSpansMatch(agentControls, spans => { - const entry = verifyHttpRootEntry({ spans, url: '/start', instanaAppControls }); + const entry = expectExactlyOneMatching(spans, [ + span => expect(span.n).to.equal('node.http.server'), + span => expect(span.k).to.equal(constants.ENTRY), + span => expect(span.data.http.url).to.equal('/start'), + span => expect(span.data.http.host).to.equal(`localhost:${instanaAppControls.getPort()}`) + ]); expect(entry.data.sdk).to.not.exist; }) )); @@ -1217,7 +1222,7 @@ module.exports = function (name, version, isLatest, mode) { const exit = expectExactlyOneMatching(spans, [ span => expect(span.n).to.equal('node.http.client'), span => expect(span.k).to.equal(constants.EXIT), - span => expect(span.data.http.host).to.include(`${otherVendorAppPort}`) + span => expect(span.data.http.url).to.include(`${otherVendorAppPort}`) ]); expect(exit.data.sdk.custom.tags.userId).to.equal('alice'); expect(exit.data.sdk.custom.tags.requestId).to.equal('req-42'); From f3d040b1c98337361949ea6e5bb02362897aaa17 Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Mon, 21 Sep 2026 16:16:28 +0200 Subject: [PATCH 17/18] test: added another test --- .../integration/misc/w3c_trace_context/test_base.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/collector/test/integration/misc/w3c_trace_context/test_base.js b/packages/collector/test/integration/misc/w3c_trace_context/test_base.js index bd5afc93dc..fc1d8b55a3 100644 --- a/packages/collector/test/integration/misc/w3c_trace_context/test_base.js +++ b/packages/collector/test/integration/misc/w3c_trace_context/test_base.js @@ -1276,6 +1276,17 @@ module.exports = function (name, version, isLatest, mode) { expect(response.w3cTraceContext.receivedHeaders.baggage).to.equal('userId=alice;meta=1,requestId=req-42'); })); + it('should propagate the full incoming baggage header to downstream even when only subset is captured', () => + startRequest({ + app: baggageAppControls, + depth: 1, + withSpecHeaders: 'valid-sampled-with-random-trace-id', + withBaggageHeader: 'userId=alice,extraKey=extraVal;prop=1' + }).then(response => { + response = response && response.body ? JSON.parse(response.body) : response; + expect(response.w3cTraceContext.receivedHeaders.baggage).to.equal('userId=alice,extraKey=extraVal;prop=1'); + })); + it('should propagate baggage set via instana.sdk.setBaggage to downstream services', () => startRequest({ app: instanaAppControls, From aa20078bcfa1922b938c95fe4eef70cec4937e20 Mon Sep 17 00:00:00 2001 From: kirrg001 Date: Tue, 22 Sep 2026 12:05:30 +0200 Subject: [PATCH 18/18] chore: cleanup --- .../test/tracing/instrumentation/baggage_propagation_test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/test/tracing/instrumentation/baggage_propagation_test.js b/packages/core/test/tracing/instrumentation/baggage_propagation_test.js index 3c26f35e19..ca2907c528 100644 --- a/packages/core/test/tracing/instrumentation/baggage_propagation_test.js +++ b/packages/core/test/tracing/instrumentation/baggage_propagation_test.js @@ -81,8 +81,6 @@ describe('baggage propagation – httpClient', () => { cls.ns.run(() => { cls.setBaggage('userId=alice'); - // getBaggage still returns the value in CLS — the client just won't write it - // to outgoing headers. The propagation guard is inside httpClient. expect(cls.getBaggage()).to.equal('userId=alice'); }); });