diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 9894e059b716..ffa5c826e274 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -123,7 +123,7 @@ function _createWrappedRequestMethodFactory( `[Tracing] Adding sentry-trace header ${sentryTraceHeader} to outgoing request to ${requestUrl}: `, ); - const headerBaggageString = requestOptions.headers && (requestOptions.headers.baggage as string); + const headerBaggageString = requestOptions.headers && requestOptions.headers.baggage; requestOptions.headers = { ...requestOptions.headers, diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 2dabfb31466e..60b831a327fe 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -36,7 +36,7 @@ export type {} from './globals'; export type { Hub } from './hub'; export type { Integration, IntegrationClass } from './integration'; export type { Mechanism } from './mechanism'; -export type { ExtractedNodeRequestData, Primitive, WorkerLocation } from './misc'; +export type { ExtractedNodeRequestData, HttpHeaderValue, Primitive, WorkerLocation } from './misc'; export type { ClientOptions, Options } from './options'; export type { Package } from './package'; export type { PolymorphicEvent } from './polymorphics'; diff --git a/packages/types/src/misc.ts b/packages/types/src/misc.ts index 916aaceb5a4a..3476befa78c6 100644 --- a/packages/types/src/misc.ts +++ b/packages/types/src/misc.ts @@ -63,3 +63,5 @@ export interface WorkerLocation { } export type Primitive = number | string | boolean | bigint | symbol | null | undefined; + +export type HttpHeaderValue = string | string[] | number | null; diff --git a/packages/utils/src/baggage.ts b/packages/utils/src/baggage.ts index a3ad316a8026..2b835a56311a 100644 --- a/packages/utils/src/baggage.ts +++ b/packages/utils/src/baggage.ts @@ -1,5 +1,7 @@ import { Baggage, BaggageObj, TraceparentData } from '@sentry/types'; +import { HttpHeaderValue } from '@sentry/types'; +import { isString } from './is'; import { logger } from './logger'; export const BAGGAGE_HEADER_NAME = 'baggage'; @@ -89,9 +91,28 @@ export function serializeBaggage(baggage: Baggage): string { }, baggage[1]); } -/** Parse a baggage header from a string and return a Baggage object */ -export function parseBaggageString(inputBaggageString: string): Baggage { - return inputBaggageString.split(',').reduce( +/** Parse a baggage header from a string or a string array and return a Baggage object */ +export function parseBaggageHeader(inputBaggageValue: HttpHeaderValue): Baggage { + // Adding this check here because we got reports of this function failing due to the input value + // not being a string. This debug log might help us determine what's going on here. + if ((!Array.isArray(inputBaggageValue) && !isString(inputBaggageValue)) || typeof inputBaggageValue === 'number') { + __DEBUG_BUILD__ && + logger.warn( + '[parseBaggageHeader] Received input value of incompatible type: ', + typeof inputBaggageValue, + inputBaggageValue, + ); + + // Gonna early-return an empty baggage object so that we don't fail later on + return createBaggage({}, ''); + } + + const baggageEntries = (isString(inputBaggageValue) ? inputBaggageValue : inputBaggageValue.join(',')) + .split(',') + .map(entry => entry.trim()) + .filter(entry => entry !== ''); + + return baggageEntries.reduce( ([baggageObj, baggageString], curr) => { const [key, val] = curr.split('='); if (SENTRY_BAGGAGE_KEY_PREFIX_REGEX.test(key)) { @@ -122,16 +143,17 @@ export function parseBaggageString(inputBaggageString: string): Baggage { * it would only affect parts of the sentry baggage (@see Baggage interface). * * @param incomingBaggage the baggage header of the incoming request that might contain sentry entries - * @param headerBaggageString possibly existing baggage header string added from a third party to request headers + * @param thirdPartyBaggageHeader possibly existing baggage header string or string[] added from a third + * party to the request headers * * @return a merged and serialized baggage string to be propagated with the outgoing request */ -export function mergeAndSerializeBaggage(incomingBaggage?: Baggage, headerBaggageString?: string): string { - if (!incomingBaggage && !headerBaggageString) { +export function mergeAndSerializeBaggage(incomingBaggage?: Baggage, thirdPartyBaggageHeader?: HttpHeaderValue): string { + if (!incomingBaggage && !thirdPartyBaggageHeader) { return ''; } - const headerBaggage = (headerBaggageString && parseBaggageString(headerBaggageString)) || undefined; + const headerBaggage = (thirdPartyBaggageHeader && parseBaggageHeader(thirdPartyBaggageHeader)) || undefined; const thirdPartyHeaderBaggage = headerBaggage && getThirdPartyBaggage(headerBaggage); const finalBaggage = createBaggage( @@ -150,14 +172,14 @@ export function mergeAndSerializeBaggage(incomingBaggage?: Baggage, headerBaggag * * Extracted this logic to a function because it's duplicated in a lot of places. * - * @param rawBaggageString + * @param rawBaggageValue * @param sentryTraceHeader */ export function parseBaggageSetMutability( - rawBaggageString: string | false | undefined | null, + rawBaggageValue: HttpHeaderValue | false | undefined, sentryTraceHeader: TraceparentData | string | false | undefined | null, ): Baggage { - const baggage = parseBaggageString(rawBaggageString || ''); + const baggage = parseBaggageHeader(rawBaggageValue || ''); if (!isSentryBaggageEmpty(baggage) || (sentryTraceHeader && isSentryBaggageEmpty(baggage))) { setBaggageImmutable(baggage); } diff --git a/packages/utils/test/baggage.test.ts b/packages/utils/test/baggage.test.ts index ff93d0d37f93..3160cc21487c 100644 --- a/packages/utils/test/baggage.test.ts +++ b/packages/utils/test/baggage.test.ts @@ -7,8 +7,8 @@ import { isBaggageMutable, isSentryBaggageEmpty, mergeAndSerializeBaggage, + parseBaggageHeader, parseBaggageSetMutability, - parseBaggageString, serializeBaggage, setBaggageImmutable, setBaggageValue, @@ -97,9 +97,10 @@ describe('Baggage', () => { }); }); - describe('parseBaggageString', () => { + describe('parseBaggageHeader', () => { it.each([ ['parses an empty string', '', createBaggage({})], + ['parses a blank string', ' ', createBaggage({})], [ 'parses sentry values into baggage', 'sentry-environment=production,sentry-release=10.0.2', @@ -113,8 +114,32 @@ describe('Baggage', () => { 'userId=alice,serverNode=DF%2028,isProduction=false', ), ], - ])('%s', (_: string, baggageString, baggage) => { - expect(parseBaggageString(baggageString)).toEqual(baggage); + [ + 'parses arbitrary baggage headers from string with empty and blank entries', + 'userId=alice, serverNode=DF%2028 , isProduction=false, ,,sentry-environment=production,,sentry-release=10.0.2', + createBaggage( + { environment: 'production', release: '10.0.2' }, + 'userId=alice,serverNode=DF%2028,isProduction=false', + ), + ], + [ + 'parses a string array', + ['userId=alice', 'sentry-environment=production', 'foo=bar'], + createBaggage({ environment: 'production' }, 'userId=alice,foo=bar'), + ], + [ + 'parses a string array with items containing multiple entries', + ['userId=alice, userName=bob', 'sentry-environment=production,sentry-release=1.0.1', 'foo=bar'], + createBaggage({ environment: 'production', release: '1.0.1' }, 'userId=alice,userName=bob,foo=bar'), + ], + [ + 'parses a string array with empty/blank entries', + ['', 'sentry-environment=production,sentry-release=1.0.1', ' ', 'foo=bar'], + createBaggage({ environment: 'production', release: '1.0.1' }, 'foo=bar'), + ], + ['ignorese other input types than string and string[]', 42, createBaggage({}, '')], + ])('%s', (_: string, baggageValue, baggage) => { + expect(parseBaggageHeader(baggageValue)).toEqual(baggage); }); });