diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/caseInsensitiveTargets/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/caseInsensitiveTargets/init.js new file mode 100644 index 000000000000..602ff7507dc2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/caseInsensitiveTargets/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracePropagationTargets: ['sentry-test-Site.example/String', /^http:\/\/sentry-test-site\.EXAMPLE\/regex/], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/caseInsensitiveTargets/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/caseInsensitiveTargets/subject.js new file mode 100644 index 000000000000..c043f0945780 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/caseInsensitiveTargets/subject.js @@ -0,0 +1,5 @@ +// The request URLs and the configured targets intentionally disagree on casing in both directions. +// These requests never resolve, so each is fired independently rather than chained. +fetch('http://sentry-test-Site.example/string/0').catch(() => {}); +fetch('http://sentry-test-site.example/REGEX/1').catch(() => {}); +fetch('http://sentry-test-site.example/no-match/2').catch(() => {}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/caseInsensitiveTargets/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/caseInsensitiveTargets/test.ts new file mode 100644 index 000000000000..96c10a678c63 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/tracePropagationTargets/caseInsensitiveTargets/test.ts @@ -0,0 +1,35 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../../utils/helpers'; + +sentryTest( + 'should attach tracing headers to requests whose casing differs from tracePropagationTargets', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const [, stringTargetRequest, regexTargetRequest, noMatchRequest] = await Promise.all([ + page.goto(url), + page.waitForRequest('http://sentry-test-site.example/string/0'), + page.waitForRequest('http://sentry-test-site.example/REGEX/1'), + page.waitForRequest('http://sentry-test-site.example/no-match/2'), + ]); + + expect(stringTargetRequest.headers()).toMatchObject({ + 'sentry-trace': expect.any(String), + baggage: expect.any(String), + }); + + expect(regexTargetRequest.headers()).toMatchObject({ + 'sentry-trace': expect.any(String), + baggage: expect.any(String), + }); + + const noMatchHeaders = noMatchRequest.headers(); + expect(noMatchHeaders['sentry-trace']).toBeUndefined(); + expect(noMatchHeaders['baggage']).toBeUndefined(); + }, +); diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/case-insensitive/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/case-insensitive/scenario.ts new file mode 100644 index 000000000000..8456ec50072c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/case-insensitive/scenario.ts @@ -0,0 +1,36 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + // The casing here intentionally disagrees with the casing of the requested URLs below. + tracePropagationTargets: [/\/API\/Regex/, 'api/String'], + integrations: [], + transport: loggingTransport, +}); + +import * as http from 'http'; + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +Sentry.startSpan({ name: 'test_span' }, async () => { + await makeHttpRequest(`${process.env.SERVER_URL}/api/regex`); + await makeHttpRequest(`${process.env.SERVER_URL}/API/STRING`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/no-match`); +}); + +function makeHttpRequest(url: string): Promise { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/case-insensitive/test.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/case-insensitive/test.ts new file mode 100644 index 000000000000..46eba247aa1b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/case-insensitive/test.ts @@ -0,0 +1,34 @@ +import { createTestServer } from '@sentry-internal/test-utils'; +import { expect, test } from 'vitest'; +import { createRunner } from '../../../../utils/runner'; + +test('tracePropagationTargets match regardless of casing', async () => { + expect.assertions(9); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/regex', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1'); + }) + .get('/API/STRING', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1'); + }) + .get('/api/no-match', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .start(); + + await createRunner(__dirname, 'scenario.ts') + .withEnv({ SERVER_URL }) + .expect({ + // The specific envelope contents are covered elsewhere; this suite is about the request headers above. + span: {}, + }) + .start() + .completed(); + closeTestServer(); +}); diff --git a/docs/migration/v11-end-state.md b/docs/migration/v11-end-state.md index 7ebb9724d7ad..4c660c5b7103 100644 --- a/docs/migration/v11-end-state.md +++ b/docs/migration/v11-end-state.md @@ -580,7 +580,23 @@ Sentry.init({ Affected SDKs: All SDKs. -String and regular-expression matching for `tracePropagationTargets` is now case-insensitive. +String and regular-expression matching for `tracePropagationTargets` is now case-insensitive. Previously a target had to +match the casing of the outgoing request URL exactly. In browsers this was especially surprising, because the URL is +normalized with `new URL()` before matching, which lower-cases the origin: a target written with the same casing as the +request, such as `'myApi.com'` or `/^myApi\.com/`, could therefore never match a request to `https://myApi.com`. + +```js +Sentry.init({ + // In a browser, neither of these matched a request to `https://myApi.com` in v10. In v11 both do. + tracePropagationTargets: ['myApi.com', /^https:\/\/myApi\.com/], +}); +``` + +If you relied on case-sensitive matching to distinguish between two targets, narrow the target so it no longer depends +on casing, or use `tracePropagationTargets` in combination with a more specific path. + +As part of this, the `g` and `y` flags are ignored on `tracePropagationTargets` regular expressions. These flags made +matching stateful via `lastIndex`, so a target like `/myApi\.com/g` previously matched only every other request. ### Span attribute changes diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index c1fe9f1fe0df..2ba87f347d67 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -19,6 +19,7 @@ import { hasSpansEnabled, hasSpanStreamingEnabled, instrumentFetchRequest, + matchesTracePropagationTargets, parseUrl, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -27,7 +28,6 @@ import { spanIsIgnored, spanToJSON, startInactiveSpan, - stringMatchesSomePattern, stripDataUrlContent, stripUrlQueryAndFragment, timestampInSeconds, @@ -65,6 +65,7 @@ export interface RequestInstrumentationOptions { * * If any of the two match any of the provided values, tracing headers will be attached to the outgoing request. * Both, the string values, and the RegExes you provide in the array will match if they partially match the URL or pathname. + * Matching is case-insensitive, so `'myApi.com'` and `/^myApi\.com/` both match a request to `https://myapi.com`. * * Examples: * - `tracePropagationTargets: [/^\/api/]` and request to `https://same-origin.com/api/posts`: @@ -281,7 +282,7 @@ export function shouldAttachHeaders( if (!tracePropagationTargets) { return isRelativeSameOriginRequest; } else { - return stringMatchesSomePattern(targetUrl, tracePropagationTargets); + return matchesTracePropagationTargets(targetUrl, tracePropagationTargets); } } else { let resolvedUrl; @@ -300,8 +301,8 @@ export function shouldAttachHeaders( return isSameOriginRequest; } else { return ( - stringMatchesSomePattern(resolvedUrl.toString(), tracePropagationTargets) || - (isSameOriginRequest && stringMatchesSomePattern(resolvedUrl.pathname, tracePropagationTargets)) + matchesTracePropagationTargets(resolvedUrl.toString(), tracePropagationTargets) || + (isSameOriginRequest && matchesTracePropagationTargets(resolvedUrl.pathname, tracePropagationTargets)) ); } } diff --git a/packages/browser/test/tracing/request.test.ts b/packages/browser/test/tracing/request.test.ts index c5e2b9a859dd..6b223740ee54 100644 --- a/packages/browser/test/tracing/request.test.ts +++ b/packages/browser/test/tracing/request.test.ts @@ -294,6 +294,16 @@ describe('shouldAttachHeaders', () => { ['https://not-my-origin.com/api', 'api', true], ['https://my-origin.com?my-query', 'my-query', true], ['https://not-my-origin.com?my-query', 'my-query', true], + + // matching is case-insensitive in both directions, because `new URL()` lower-cases the origin + ['https://MY-ORIGIN.com', 'my-origin', true], + ['https://my-origin.com', 'MY-ORIGIN', true], + ['https://my-origin.com', /^https:\/\/MY-ORIGIN\.com\//, true], + ['https://MY-ORIGIN.com', /^https:\/\/my-origin\.com\//, true], + ['https://my-origin.com/API/my-route', '/api/', true], + ['https://my-origin.com/api/my-route', '/API/', true], + ['https://my-origin.com/API/my-route', /^\/api\//, true], + ['https://MY-ORIGIN.com', 'not-my-origin', false], // still no match on a genuinely different target ])( 'for url %j and tracePropagationTarget %j on page "https://my-origin.com/api/my-route" should return %j', (url, matcher, result) => { @@ -439,6 +449,13 @@ describe('shouldAttachHeaders', () => { ['https://not-my-origin.com/api', 'api', true], ['https://my-origin.com?my-query', 'my-query', true], ['https://not-my-origin.com?my-query', 'my-query', true], + + // matching is case-insensitive in both directions, because `new URL()` lower-cases the origin + ['https://MY-ORIGIN.com', 'my-origin', true], + ['https://my-origin.com', 'MY-ORIGIN', true], + ['https://my-origin.com/', /^https:\/\/MY-ORIGIN\.com\//, true], + ['https://MY-ORIGIN.com/', /^https:\/\/my-origin\.com\//, true], + ['https://MY-ORIGIN.com', 'not-my-origin', false], // still no match on a genuinely different target ])('for url %j and tracePropagationTarget %j should return %j', (url, matcher, result) => { expect(shouldAttachHeaders(url, [matcher])).toBe(result); }); diff --git a/packages/bun/src/integrations/fetch.ts b/packages/bun/src/integrations/fetch.ts index d002ee4875c9..b908ccaf2e25 100644 --- a/packages/bun/src/integrations/fetch.ts +++ b/packages/bun/src/integrations/fetch.ts @@ -15,7 +15,7 @@ import { instrumentFetchRequest, isSentryRequestUrl, LRUMap, - stringMatchesSomePattern, + shouldPropagateTraceForUrl, } from '@sentry/core'; const INTEGRATION_NAME = 'Fetch' as const; @@ -53,20 +53,7 @@ const _fetchIntegration = ((options: FetchOptions = {}) => { return false; } - const clientOptions = client.getOptions(); - - if (clientOptions.tracePropagationTargets === undefined) { - return true; - } - - const cachedDecision = _headersUrlMap.get(url); - if (cachedDecision !== undefined) { - return cachedDecision; - } - - const decision = stringMatchesSomePattern(url, clientOptions.tracePropagationTargets); - _headersUrlMap.set(url, decision); - return decision; + return shouldPropagateTraceForUrl(url, client.getOptions().tracePropagationTargets, _headersUrlMap); } /** Helper that wraps shouldCreateSpanForRequest option */ diff --git a/packages/cloudflare/src/integrations/fetch.ts b/packages/cloudflare/src/integrations/fetch.ts index fab9501e19e5..585377e9d76d 100644 --- a/packages/cloudflare/src/integrations/fetch.ts +++ b/packages/cloudflare/src/integrations/fetch.ts @@ -15,7 +15,7 @@ import { instrumentFetchRequest, isSentryRequestUrl, LRUMap, - stringMatchesSomePattern, + shouldPropagateTraceForUrl, } from '@sentry/core'; const INTEGRATION_NAME = 'Fetch' as const; @@ -53,20 +53,7 @@ const _fetchIntegration = ((options: Partial = {}) => { return false; } - const clientOptions = client.getOptions(); - - if (clientOptions.tracePropagationTargets === undefined) { - return true; - } - - const cachedDecision = _headersUrlMap.get(url); - if (cachedDecision !== undefined) { - return cachedDecision; - } - - const decision = stringMatchesSomePattern(url, clientOptions.tracePropagationTargets); - _headersUrlMap.set(url, decision); - return decision; + return shouldPropagateTraceForUrl(url, client.getOptions().tracePropagationTargets, _headersUrlMap); } /** Helper that wraps shouldCreateSpanForRequest option */ diff --git a/packages/cloudflare/test/integrations/fetch.test.ts b/packages/cloudflare/test/integrations/fetch.test.ts index 11dbaa18916b..c2cdda44d182 100644 --- a/packages/cloudflare/test/integrations/fetch.test.ts +++ b/packages/cloudflare/test/integrations/fetch.test.ts @@ -66,6 +66,10 @@ describe('WinterCGFetch instrumentation', () => { expect(shouldAttachTraceData('http://my-website.com/')).toBe(true); expect(shouldAttachTraceData('https://www.3rd-party-website.at/')).toBe(false); + // tracePropagationTargets match regardless of casing + expect(shouldAttachTraceData('http://MY-WEBSITE.com/')).toBe(true); + expect(shouldAttachTraceData('https://WWW.3RD-PARTY-WEBSITE.at/')).toBe(false); + expect(shouldCreateSpan('http://my-website.com/')).toBe(true); expect(shouldCreateSpan('https://www.3rd-party-website.at/')).toBe(true); }); diff --git a/packages/core/src/shared-exports.ts b/packages/core/src/shared-exports.ts index 0ba96d4da8e5..27098bb5df9c 100644 --- a/packages/core/src/shared-exports.ts +++ b/packages/core/src/shared-exports.ts @@ -118,7 +118,7 @@ export { _setSpanForScope as _INTERNAL_setSpanForScope } from './utils/spanOnSco export { parseSampleRate } from './utils/parseSampleRate'; export { applySdkMetadata } from './utils/sdkMetadata'; export { getTraceData } from './utils/traceData'; -export { shouldPropagateTraceForUrl } from './utils/tracePropagationTargets'; +export { matchesTracePropagationTargets, shouldPropagateTraceForUrl } from './utils/tracePropagationTargets'; export { getTraceMetaTags } from './utils/meta'; export { debounce } from './utils/debounce'; export { uniq } from './utils/array'; diff --git a/packages/core/src/types/options.ts b/packages/core/src/types/options.ts index 3821d6d050da..8ea3944f1613 100644 --- a/packages/core/src/types/options.ts +++ b/packages/core/src/types/options.ts @@ -455,6 +455,7 @@ export interface ClientOptions(); + +/** + * Returns an equivalent RegExp that ignores case and is safe to `test()` repeatedly. + * + * The `g` and `y` flags are dropped because they make `test()` stateful via `lastIndex`, which would make a target + * match only every other request. Results are cached since targets are matched once per outgoing request. + */ +function normalizeRegExpTarget(pattern: RegExp): RegExp { + const flags = `${pattern.flags.replace(/[gy]/g, '')}${pattern.ignoreCase ? '' : 'i'}`; + if (flags === pattern.flags) { + return pattern; + } + + const cached = NORMALIZED_REGEXP_CACHE.get(pattern); + if (cached) { + return cached; + } + + const normalizedPattern = new RegExp(pattern.source, flags); + NORMALIZED_REGEXP_CACHE.set(pattern, normalizedPattern); + return normalizedPattern; +} + +/** + * Check if a URL matches any of the given `tracePropagationTargets`. + * + * Matching is case-insensitive: URL normalization (e.g. `new URL()`) lower-cases the origin, so a target + * written with the same casing as the request (`'myApi.com'`, `/^myApi\.com/`) would otherwise never match. + */ +export function matchesTracePropagationTargets(url: string, tracePropagationTargets: TracePropagationTargets): boolean { + const lowerCaseUrl = url.toLowerCase(); + + for (const target of tracePropagationTargets) { + if (isString(target)) { + if (lowerCaseUrl.includes(target.toLowerCase())) { + return true; + } + } else if (isRegExp(target) && normalizeRegExpTarget(target).test(url)) { + return true; + } + } + + return false; +} + /** * Check if a given URL should be propagated to or not. * If no url is defined, or no trace propagation targets are defined, this will always return `true`. @@ -27,7 +74,7 @@ export function shouldPropagateTraceForUrl( return cachedDecision; } - const decision = stringMatchesSomePattern(url, tracePropagationTargets); + const decision = matchesTracePropagationTargets(url, tracePropagationTargets); decisionMap?.set(url, decision); DEBUG_BUILD && !decision && debug.log(NOT_PROPAGATED_MESSAGE, url); diff --git a/packages/core/test/lib/utils/tracePropagationTargets.test.ts b/packages/core/test/lib/utils/tracePropagationTargets.test.ts new file mode 100644 index 000000000000..6e7d973e115a --- /dev/null +++ b/packages/core/test/lib/utils/tracePropagationTargets.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from 'vitest'; +import type { TracePropagationTargets } from '../../../src/types/tracing'; +import { LRUMap } from '../../../src/utils/lru'; +import { matchesTracePropagationTargets, shouldPropagateTraceForUrl } from '../../../src/utils/tracePropagationTargets'; + +describe('matchesTracePropagationTargets', () => { + it.each([ + // string targets, matching casing + ['https://myapi.com/v1', ['myapi.com/'], true], + ['https://myapi.com/v1', ['other.com/'], false], + + // string targets, mismatched casing + ['https://myapi.com/v1', ['myApi.com/'], true], + ['https://myApi.com/v1', ['myapi.com/'], true], + ['https://MYAPI.COM/v1', ['myapi.com/'], true], + ['https://myapi.com/API/v1', ['/api/'], true], + ['https://myapi.com/api/v1', ['/API/'], true], + + // regex targets, matching casing + [String.raw`https://myapi.com/v1`, [/^https:\/\/myapi\.com\//], true], + [String.raw`https://myapi.com/v1`, [/^https:\/\/other\.com\//], false], + + // regex targets, mismatched casing + [String.raw`https://myapi.com/v1`, [/^https:\/\/myApi\.com\//], true], + [String.raw`https://myApi.com/v1`, [/^https:\/\/myapi\.com\//], true], + [String.raw`https://myapi.com/API/v1`, [/\/api\//], true], + + // regexes that already ignore case keep working + [String.raw`https://myapi.com/v1`, [/^https:\/\/myAPI\.com\//i], true], + + // a non-matching target stays non-matching regardless of casing + [String.raw`https://myapi.com/v1`, ['MYOTHERAPI.COM/', /^https:\/\/OTHER\.com\//], false], + + // mixed target lists + ['https://myapi.com/v1', ['other.com/', /^https:\/\/myApi\.com\//], true], + + // empty target list never matches + ['https://myapi.com/v1', [], false], + ])('for url %j and targets %j returns %j', (url, targets, expected) => { + expect(matchesTracePropagationTargets(url, targets)).toBe(expected); + }); + + it.each([[123], [null], [undefined], [{}], [() => true]])( + 'returns false instead of throwing for the unsupported target %j', + target => { + // `tracePropagationTargets` is typed as `(string | RegExp)[]`, but it is frequently set from plain JS, + // and throwing here would break the instrumented request itself. + expect(matchesTracePropagationTargets('https://myapi.com', [target] as unknown as TracePropagationTargets)).toBe( + false, + ); + }, + ); + + it('matches boxed strings', () => { + // eslint-disable-next-line no-new-wrappers + const target = new String('myApi.com/') as unknown as string; + + expect(matchesTracePropagationTargets('https://myapi.com/v1', [target])).toBe(true); + }); + + it('does not mutate the flags of the passed regex', () => { + const target = /^https:\/\/myApi\.com\//; + + expect(matchesTracePropagationTargets('https://myapi.com/v1', [target])).toBe(true); + expect(target.ignoreCase).toBe(false); + expect(target.flags).toBe(''); + }); + + it('preserves other flags when adding case insensitivity', () => { + expect(matchesTracePropagationTargets('https://myapi.com/v1', [/MYAPI\.com\/v1$/m])).toBe(true); + }); + + it('matches consistently across repeated calls for the same regex', () => { + const target = /^https:\/\/myApi\.com\//g; + + expect(matchesTracePropagationTargets('https://myapi.com/v1', [target])).toBe(true); + expect(matchesTracePropagationTargets('https://myapi.com/v2', [target])).toBe(true); + }); +}); + +describe('shouldPropagateTraceForUrl', () => { + it('propagates when no targets are defined', () => { + expect(shouldPropagateTraceForUrl('https://myapi.com', undefined)).toBe(true); + }); + + it('propagates when no url is defined', () => { + expect(shouldPropagateTraceForUrl(undefined, ['myapi.com/'])).toBe(true); + }); + + it('matches case-insensitively', () => { + expect(shouldPropagateTraceForUrl('https://myapi.com/v1', ['myApi.com/'])).toBe(true); + expect(shouldPropagateTraceForUrl('https://myApi.com/v1', [/^https:\/\/myapi\.com\//])).toBe(true); + }); + + it('caches the decision per url', () => { + const decisionMap = new LRUMap(10); + + expect(shouldPropagateTraceForUrl('https://myapi.com/v1', ['myApi.com/'], decisionMap)).toBe(true); + expect(decisionMap.get('https://myapi.com/v1')).toBe(true); + expect(shouldPropagateTraceForUrl('https://myapi.com/v1', ['myApi.com/'], decisionMap)).toBe(true); + }); +}); diff --git a/packages/vercel-edge/src/integrations/wintercg-fetch.ts b/packages/vercel-edge/src/integrations/wintercg-fetch.ts index cef56eeaaabc..217efe00df2d 100644 --- a/packages/vercel-edge/src/integrations/wintercg-fetch.ts +++ b/packages/vercel-edge/src/integrations/wintercg-fetch.ts @@ -15,7 +15,7 @@ import { instrumentFetchRequest, isSentryRequestUrl, LRUMap, - stringMatchesSomePattern, + shouldPropagateTraceForUrl, } from '@sentry/core'; const INTEGRATION_NAME = 'WinterCGFetch' as const; @@ -53,20 +53,7 @@ const _winterCGFetch = ((options: Partial = {}) => { return false; } - const clientOptions = client.getOptions(); - - if (clientOptions.tracePropagationTargets === undefined) { - return true; - } - - const cachedDecision = _headersUrlMap.get(url); - if (cachedDecision !== undefined) { - return cachedDecision; - } - - const decision = stringMatchesSomePattern(url, clientOptions.tracePropagationTargets); - _headersUrlMap.set(url, decision); - return decision; + return shouldPropagateTraceForUrl(url, client.getOptions().tracePropagationTargets, _headersUrlMap); } /** Helper that wraps shouldCreateSpanForRequest option */ diff --git a/packages/vercel-edge/test/wintercg-fetch.test.ts b/packages/vercel-edge/test/wintercg-fetch.test.ts index d430b46df831..9d9ffcd755f4 100644 --- a/packages/vercel-edge/test/wintercg-fetch.test.ts +++ b/packages/vercel-edge/test/wintercg-fetch.test.ts @@ -66,6 +66,10 @@ describe('WinterCGFetch instrumentation', () => { expect(shouldAttachTraceData('http://my-website.com/')).toBe(true); expect(shouldAttachTraceData('https://www.3rd-party-website.at/')).toBe(false); + // tracePropagationTargets match regardless of casing + expect(shouldAttachTraceData('http://MY-WEBSITE.com/')).toBe(true); + expect(shouldAttachTraceData('https://WWW.3RD-PARTY-WEBSITE.at/')).toBe(false); + expect(shouldCreateSpan('http://my-website.com/')).toBe(true); expect(shouldCreateSpan('https://www.3rd-party-website.at/')).toBe(true); });