diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index f64f2b11d114..563c8ce599c3 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -618,7 +618,7 @@ export abstract class BaseClient implements Client { throw new SentryError('`beforeSend` returned `null`, will not send event.'); } - const session = scope && scope.getSession && scope.getSession(); + const session = scope && scope.getSession(); if (!isTransaction && session) { this._updateSessionFromEvent(session, processedEvent); } diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index 2f69884e8bc3..149d98d5242e 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -2,6 +2,7 @@ import { DsnComponents, Event, EventEnvelope, + EventEnvelopeHeaders, EventItem, SdkInfo, SdkMetadata, @@ -10,7 +11,15 @@ import { SessionEnvelope, SessionItem, } from '@sentry/types'; -import { createEnvelope, dsnToString } from '@sentry/utils'; +import { + BaggageObj, + createBaggage, + createEnvelope, + dropUndefinedKeys, + dsnToString, + isBaggageEmpty, + serializeBaggage, +} from '@sentry/utils'; /** Extract sdk info from from the API metadata */ function getSdkMetadataForEnvelopeHeader(metadata?: SdkMetadata): SdkInfo | undefined { @@ -101,12 +110,8 @@ export function createEventEnvelope( // TODO: This is NOT part of the hack - DO NOT DELETE delete event.sdkProcessingMetadata; - const envelopeHeaders = { - event_id: event.event_id as string, - sent_at: new Date().toISOString(), - ...(sdkInfo && { sdk: sdkInfo }), - ...(!!tunnel && { dsn: dsnToString(dsn) }), - }; + const envelopeHeaders = createEventEnvelopeHeaders(event, sdkInfo, tunnel, dsn); + const eventItem: EventItem = [ { type: eventType, @@ -116,3 +121,31 @@ export function createEventEnvelope( ]; return createEnvelope(envelopeHeaders, [eventItem]); } + +function createEventEnvelopeHeaders( + event: Event, + sdkInfo: SdkInfo | undefined, + tunnel: string | undefined, + dsn: DsnComponents, +): EventEnvelopeHeaders { + const baggage = + event.type === 'transaction' && + createBaggage( + dropUndefinedKeys({ + environment: event.environment, + release: event.release, + transaction: event.transaction, + userid: event.user && event.user.id, + // user.segment currently doesn't exist explicitly in interface User (just as a record key) + usersegment: event.user && event.user.segment, + } as BaggageObj), + ); + + return { + event_id: event.event_id as string, + sent_at: new Date().toISOString(), + ...(sdkInfo && { sdk: sdkInfo }), + ...(!!tunnel && { dsn: dsnToString(dsn) }), + ...(baggage && !isBaggageEmpty(baggage) && { baggage: serializeBaggage(baggage) }), + }; +} diff --git a/packages/core/test/lib/envelope.test.ts b/packages/core/test/lib/envelope.test.ts new file mode 100644 index 000000000000..a5c8cb8ae06a --- /dev/null +++ b/packages/core/test/lib/envelope.test.ts @@ -0,0 +1,54 @@ +import { DsnComponents, Event } from '@sentry/types'; + +import { createEventEnvelope } from '../../src/envelope'; + +const testDsn: DsnComponents = { protocol: 'https', projectId: 'abc', host: 'testry.io' }; + +describe('createEventEnvelope', () => { + describe('baggage header', () => { + it("doesn't add baggage header if event is not a transaction", () => { + const event: Event = {}; + const envelopeHeaders = createEventEnvelope(event, testDsn)[0]; + + expect(envelopeHeaders).toBeDefined(); + expect(envelopeHeaders.baggage).toBeUndefined(); + }); + + it("doesn't add baggage header if no baggage data is available", () => { + const event: Event = { + type: 'transaction', + }; + const envelopeHeaders = createEventEnvelope(event, testDsn)[0]; + + expect(envelopeHeaders).toBeDefined(); + expect(envelopeHeaders.baggage).toBeUndefined(); + }); + + const testTable: Array<[string, Event, string]> = [ + ['adds only baggage item', { type: 'transaction', release: '1.0.0' }, 'sentry-release=1.0.0'], + [ + 'adds two baggage items', + { type: 'transaction', release: '1.0.0', environment: 'prod' }, + 'sentry-environment=prod,sentry-release=1.0.0', + ], + [ + 'adds all baggageitems', + { + type: 'transaction', + release: '1.0.0', + environment: 'prod', + user: { id: 'bob', segment: 'segmentA' }, + transaction: 'TX', + }, + 'sentry-environment=prod,sentry-release=1.0.0,sentry-transaction=TX,sentry-userid=bob,sentry-usersegment=segmentA', + ], + ]; + it.each(testTable)('%s', (_: string, event, serializedBaggage) => { + const envelopeHeaders = createEventEnvelope(event, testDsn)[0]; + + expect(envelopeHeaders).toBeDefined(); + expect(envelopeHeaders.baggage).toBeDefined(); + expect(envelopeHeaders.baggage).toEqual(serializedBaggage); + }); + }); +}); diff --git a/packages/integration-tests/suites/tracing/baggage/init.js b/packages/integration-tests/suites/tracing/baggage/init.js new file mode 100644 index 000000000000..5cd0764d4da5 --- /dev/null +++ b/packages/integration-tests/suites/tracing/baggage/init.js @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/browser'; +import { Integrations } from '@sentry/tracing'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [new Integrations.BrowserTracing({ tracingOrigins: [/.*/] })], + environment: 'production', + tracesSampleRate: 1, +}); + +Sentry.configureScope(scope => { + scope.setUser({ id: 'user123', segment: 'segmentB' }); + scope.setTransactionName('testTransactionBaggage'); +}); diff --git a/packages/integration-tests/suites/tracing/baggage/test.ts b/packages/integration-tests/suites/tracing/baggage/test.ts new file mode 100644 index 000000000000..5617e0d57309 --- /dev/null +++ b/packages/integration-tests/suites/tracing/baggage/test.ts @@ -0,0 +1,16 @@ +import { expect } from '@playwright/test'; +import { Event, EventEnvelopeHeaders } from '@sentry/types'; + +import { sentryTest } from '../../../utils/fixtures'; +import { envelopeHeaderRequestParser, getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; + +sentryTest('should send baggage data in transaction envelope header', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const envHeader = await getFirstSentryEnvelopeRequest(page, url, envelopeHeaderRequestParser); + + expect(envHeader.baggage).toBeDefined(); + expect(envHeader.baggage).toEqual( + 'sentry-environment=production,sentry-transaction=testTransactionBaggage,sentry-userid=user123,sentry-usersegment=segmentB', + ); +}); diff --git a/packages/integration-tests/utils/helpers.ts b/packages/integration-tests/utils/helpers.ts index 8f6e06b97d5a..34b512ec9e01 100644 --- a/packages/integration-tests/utils/helpers.ts +++ b/packages/integration-tests/utils/helpers.ts @@ -1,5 +1,5 @@ import { Page, Request } from '@playwright/test'; -import { Event } from '@sentry/types'; +import { Event, EventEnvelopeHeaders } from '@sentry/types'; const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//; @@ -11,6 +11,14 @@ const envelopeRequestParser = (request: Request | null): Event => { return envelope.split('\n').map(line => JSON.parse(line))[2]; }; +export const envelopeHeaderRequestParser = (request: Request | null): EventEnvelopeHeaders => { + // https://develop.sentry.dev/sdk/envelopes/ + const envelope = request?.postData() || ''; + + // First row of the envelop is the event payload. + return envelope.split('\n').map(line => JSON.parse(line))[0]; +}; + /** * Run script at the given path inside the test environment. * @@ -122,10 +130,11 @@ async function getMultipleSentryEnvelopeRequests( url?: string; timeout?: number; }, + requestParser: (req: Request) => T = envelopeRequestParser as (req: Request) => T, ): Promise { // TODO: This is not currently checking the type of envelope, just casting for now. // We can update this to include optional type-guarding when we have types for Envelope. - return getMultipleRequests(page, count, envelopeUrlRegex, envelopeRequestParser, options) as Promise; + return getMultipleRequests(page, count, envelopeUrlRegex, requestParser, options) as Promise; } /** @@ -136,8 +145,12 @@ async function getMultipleSentryEnvelopeRequests( * @param {string} [url] * @return {*} {Promise} */ -async function getFirstSentryEnvelopeRequest(page: Page, url?: string): Promise { - return (await getMultipleSentryEnvelopeRequests(page, 1, { url }))[0]; +async function getFirstSentryEnvelopeRequest( + page: Page, + url?: string, + requestParser: (req: Request) => T = envelopeRequestParser as (req: Request) => T, +): Promise { + return (await getMultipleSentryEnvelopeRequests(page, 1, { url }, requestParser))[0]; } /** diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index 33846f077a79..7e350b129b21 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -56,7 +56,7 @@ export type SessionItem = | BaseEnvelopeItem; export type ClientReportItem = BaseEnvelopeItem; -type EventEnvelopeHeaders = { event_id: string; sent_at: string }; +export type EventEnvelopeHeaders = { event_id: string; sent_at: string; baggage?: string }; type SessionEnvelopeHeaders = { sent_at: string }; type ClientReportEnvelopeHeaders = BaseEnvelopeHeaders; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 8fe14cd3ac16..48c2ef6ff5e4 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -15,6 +15,7 @@ export type { EnvelopeItemType, EnvelopeItem, EventEnvelope, + EventEnvelopeHeaders, EventItem, SessionEnvelope, SessionItem, diff --git a/packages/utils/src/baggage.ts b/packages/utils/src/baggage.ts index db402608a22b..f8bec69439b3 100644 --- a/packages/utils/src/baggage.ts +++ b/packages/utils/src/baggage.ts @@ -1,7 +1,7 @@ import { IS_DEBUG_BUILD } from './flags'; import { logger } from './logger'; -export type AllowedBaggageKeys = 'environment' | 'release'; // TODO: Add remaining allowed baggage keys | 'transaction' | 'userid' | 'usersegment'; +export type AllowedBaggageKeys = 'environment' | 'release' | 'userid' | 'transaction' | 'usersegment'; export type BaggageObj = Partial & Record>; /** @@ -47,6 +47,11 @@ export function setBaggageValue(baggage: Baggage, key: keyof BaggageObj, value: baggage[0][key] = value; } +/** Check if the baggage object (i.e. the first element in the tuple) is empty */ +export function isBaggageEmpty(baggage: Baggage): boolean { + return Object.keys(baggage[0]).length === 0; +} + /** Serialize a baggage object */ export function serializeBaggage(baggage: Baggage): string { return Object.keys(baggage[0]).reduce((prev, key: keyof BaggageObj) => { diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 01b875dc31c3..fd627db6f2e5 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -23,3 +23,4 @@ export * from './env'; export * from './envelope'; export * from './clientreport'; export * from './ratelimit'; +export * from './baggage'; diff --git a/packages/utils/test/baggage.test.ts b/packages/utils/test/baggage.test.ts index 08d4213f2a99..e1c91a87f5ee 100644 --- a/packages/utils/test/baggage.test.ts +++ b/packages/utils/test/baggage.test.ts @@ -1,4 +1,11 @@ -import { createBaggage, getBaggageValue, parseBaggageString, serializeBaggage, setBaggageValue } from '../src/baggage'; +import { + createBaggage, + getBaggageValue, + isBaggageEmpty, + parseBaggageString, + serializeBaggage, + setBaggageValue, +} from '../src/baggage'; describe('Baggage', () => { describe('createBaggage', () => { @@ -89,4 +96,13 @@ describe('Baggage', () => { expect(parseBaggageString(baggageString)).toEqual(baggage); }); }); + + describe('isBaggageEmpty', () => { + it.each([ + ['returns true if the modifyable part of baggage is empty', createBaggage({}), true], + ['returns false if the modifyable part of baggage is not empty', createBaggage({ release: '10.0.2' }), false], + ])('%s', (_: string, baggage, outcome) => { + expect(isBaggageEmpty(baggage)).toEqual(outcome); + }); + }); });