Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(tracing): Pass tracePropagationTargets to instrumentOutgoingRequests #6259

Merged
merged 6 commits into from Nov 22, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,10 @@
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({ tracePropagationTargets: ['http://example.com'] })],
tracesSampleRate: 1,
});
@@ -0,0 +1 @@
fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2')));
@@ -0,0 +1,27 @@
import { expect, Request } from '@playwright/test';

import { sentryTest } from '../../../../../utils/fixtures';

sentryTest(
'should attach `sentry-trace` and `baggage` header to request matching tracePropagationTargets',
async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const requests = (
await Promise.all([
page.goto(url),
Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))),
])
)[1];

expect(requests).toHaveLength(3);

requests?.forEach(async (request: Request) => {
const requestHeaders = await request.allHeaders();
expect(requestHeaders).toMatchObject({
'sentry-trace': expect.any(String),
baggage: expect.any(String),
});
});
},
);
@@ -0,0 +1,12 @@
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({ tracePropagationTargets: [], tracingOrigins: ['http://example.com'] }),
],
tracesSampleRate: 1,
});
@@ -0,0 +1 @@
fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2')));
@@ -0,0 +1,27 @@
import { expect, Request } from '@playwright/test';

import { sentryTest } from '../../../../../utils/fixtures';

sentryTest(
'[pre-v8] should prefer custom tracePropagationTargets over tracingOrigins',
async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const requests = (
await Promise.all([
page.goto(url),
Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))),
])
)[1];

expect(requests).toHaveLength(3);

requests?.forEach(async (request: Request) => {
const requestHeaders = await request.allHeaders();
expect(requestHeaders).not.toMatchObject({
'sentry-trace': expect.any(String),
baggage: expect.any(String),
});
});
},
);
@@ -0,0 +1,10 @@
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: ['http://example.com'] })],
tracesSampleRate: 1,
});
@@ -0,0 +1 @@
fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2')));
@@ -0,0 +1,27 @@
import { expect, Request } from '@playwright/test';

import { sentryTest } from '../../../../../utils/fixtures';

sentryTest(
'[pre-v8] should attach `sentry-trace` and `baggage` header to request matching tracingOrigins',
async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const requests = (
await Promise.all([
page.goto(url),
Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))),
])
)[1];

expect(requests).toHaveLength(3);

requests?.forEach(async (request: Request) => {
const requestHeaders = await request.allHeaders();
expect(requestHeaders).toMatchObject({
'sentry-trace': expect.any(String),
baggage: expect.any(String),
});
});
},
);
@@ -0,0 +1,10 @@
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()],
tracesSampleRate: 1,
});
@@ -0,0 +1 @@
fetch('http://localhost:4200/0').then(fetch('http://localhost:4200/1').then(fetch('http://localhost:4200/2')));
@@ -0,0 +1,27 @@
import { expect, Request } from '@playwright/test';

import { sentryTest } from '../../../../../utils/fixtures';

sentryTest(
'should attach `sentry-trace` and `baggage` header to request matching default tracePropagationTargets',
async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const requests = (
await Promise.all([
page.goto(url),
Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://localhost:4200/${idx}`))),
])
)[1];

expect(requests).toHaveLength(3);

requests?.forEach(async (request: Request) => {
const requestHeaders = await request.allHeaders();
expect(requestHeaders).toMatchObject({
'sentry-trace': expect.any(String),
baggage: expect.any(String),
});
});
},
);
@@ -0,0 +1,10 @@
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()],
tracesSampleRate: 1,
});
@@ -0,0 +1 @@
fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2')));
@@ -0,0 +1,27 @@
import { expect, Request } from '@playwright/test';

import { sentryTest } from '../../../../../utils/fixtures';

sentryTest(
'should not attach `sentry-trace` and `baggage` header to request not matching default tracePropagationTargets',
async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const requests = (
await Promise.all([
page.goto(url),
Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://example.com/${idx}`))),
])
)[1];

expect(requests).toHaveLength(3);

requests?.forEach(async (request: Request) => {
const requestHeaders = await request.allHeaders();
expect(requestHeaders).not.toMatchObject({
'sentry-trace': expect.any(String),
baggage: expect.any(String),
});
});
},
);
21 changes: 17 additions & 4 deletions packages/tracing/src/browser/browsertracing.ts
Expand Up @@ -112,7 +112,7 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions {
): void;
}

const DEFAULT_BROWSER_TRACING_OPTIONS = {
const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = {
idleTimeout: DEFAULT_IDLE_TIMEOUT,
finalTimeout: DEFAULT_FINAL_TIMEOUT,
heartbeatInterval: DEFAULT_HEARTBEAT_INTERVAL,
Expand Down Expand Up @@ -153,6 +153,15 @@ export class BrowserTracing implements Integration {
..._options,
};

// TODO (v8): remove this block after tracingOrigins is removed
// Set tracePropagationTargets to tracingOrigins if specified by the user
// In case both are specified, tracePropagationTargets takes precedence
// eslint-disable-next-line deprecation/deprecation
if (_options && !_options.tracePropagationTargets && _options.tracingOrigins) {
// eslint-disable-next-line deprecation/deprecation
this.options.tracePropagationTargets = _options.tracingOrigins;
}

const { _metricOptions } = this.options;
startTrackingWebVitals(_metricOptions && _metricOptions._reportAllChanges);
if (this.options._experiments?.enableLongTask) {
Expand All @@ -174,8 +183,7 @@ export class BrowserTracing implements Integration {
markBackgroundTransactions,
traceFetch,
traceXHR,
// eslint-disable-next-line deprecation/deprecation
tracingOrigins,
tracePropagationTargets,
shouldCreateSpanForRequest,
} = this.options;

Expand All @@ -189,7 +197,12 @@ export class BrowserTracing implements Integration {
registerBackgroundTabDetection();
}

instrumentOutgoingRequests({ traceFetch, traceXHR, tracingOrigins, shouldCreateSpanForRequest });
instrumentOutgoingRequests({
traceFetch,
traceXHR,
tracePropagationTargets,
shouldCreateSpanForRequest,
});
}

/** Create routing idle transaction. */
Expand Down
20 changes: 4 additions & 16 deletions packages/tracing/src/browser/request.ts
Expand Up @@ -113,7 +113,7 @@ export const defaultRequestInstrumentationOptions: RequestInstrumentationOptions
/** Registers span creators for xhr and fetch requests */
export function instrumentOutgoingRequests(_options?: Partial<RequestInstrumentationOptions>): void {
// eslint-disable-next-line deprecation/deprecation
const { traceFetch, traceXHR, tracingOrigins, tracePropagationTargets, shouldCreateSpanForRequest } = {
const { traceFetch, traceXHR, tracePropagationTargets, shouldCreateSpanForRequest } = {
traceFetch: defaultRequestInstrumentationOptions.traceFetch,
traceXHR: defaultRequestInstrumentationOptions.traceXHR,
..._options,
Expand All @@ -122,8 +122,7 @@ export function instrumentOutgoingRequests(_options?: Partial<RequestInstrumenta
const shouldCreateSpan =
typeof shouldCreateSpanForRequest === 'function' ? shouldCreateSpanForRequest : (_: string) => true;

const shouldAttachHeadersWithTargets = (url: string): boolean =>
shouldAttachHeaders(url, tracingOrigins, tracePropagationTargets);
const shouldAttachHeadersWithTargets = (url: string): boolean => shouldAttachHeaders(url, tracePropagationTargets);

const spans: Record<string, Span> = {};

Expand All @@ -144,20 +143,9 @@ export function instrumentOutgoingRequests(_options?: Partial<RequestInstrumenta
* A function that determines whether to attach tracing headers to a request.
* This was extracted from `instrumentOutgoingRequests` to make it easier to test shouldAttachHeaders.
* We only export this fuction for testing purposes.
*
* TODO (v8): Remove `tracingOrigins` which should drastically simplify this function.
*/
export function shouldAttachHeaders(
url: string,
tracePropagationTargets: (string | RegExp)[] | undefined,
tracingOrigins: (string | RegExp)[] | undefined,
): boolean {
// TODO (v8): Replace the entire code below with this one-liner:
// return stringMatchesSomePattern(url, tracePropagationTargets || DEFAULT_TRACE_PROPAGATION_TARGETS);
if (tracePropagationTargets || tracingOrigins) {
return stringMatchesSomePattern(url, tracePropagationTargets || tracingOrigins);
}
return stringMatchesSomePattern(url, DEFAULT_TRACE_PROPAGATION_TARGETS);
export function shouldAttachHeaders(url: string, tracePropagationTargets: (string | RegExp)[] | undefined): boolean {
return stringMatchesSomePattern(url, tracePropagationTargets || DEFAULT_TRACE_PROPAGATION_TARGETS);
}

/**
Expand Down
47 changes: 47 additions & 0 deletions packages/tracing/test/browser/browsertracing.test.ts
Expand Up @@ -34,6 +34,15 @@ jest.mock('@sentry/utils', () => {

jest.mock('../../src/browser/metrics');

const instrumentOutgoingRequestsMock = jest.fn();
jest.mock('./../../src/browser/request', () => {
const actual = jest.requireActual('./../../src/browser/request');
return {
...actual,
instrumentOutgoingRequests: (options: Partial<BrowserTracingOptions>) => instrumentOutgoingRequestsMock(options),
};
});

beforeAll(() => {
const dom = new JSDOM();
// @ts-ignore need to override global document
Expand Down Expand Up @@ -128,6 +137,7 @@ describe('BrowserTracing', () => {
expect(transaction.endTimestamp).toBe(span.endTimestamp);
});

// TODO (v8): remove these tests
describe('tracingOrigins', () => {
it('sets tracing origins if provided and does not warn', () => {
const sampleTracingOrigins = ['something'];
Expand All @@ -152,6 +162,43 @@ describe('BrowserTracing', () => {
});
});

describe('tracePropagationTargets', () => {
it('sets tracePropagationTargets if provided', () => {
const sampleTracePropagationTargets = ['something'];
const inst = createBrowserTracing(true, {
routingInstrumentation: customInstrumentRouting,
tracePropagationTargets: sampleTracePropagationTargets,
});

expect(inst.options.tracePropagationTargets).toEqual(sampleTracePropagationTargets);
});

it('sets tracePropagationTargets to an empty array and does not warn', () => {
const sampleTracePropagationTargets: string[] = [];
const inst = createBrowserTracing(true, {
routingInstrumentation: customInstrumentRouting,
tracePropagationTargets: sampleTracePropagationTargets,
});

expect(inst.options.tracePropagationTargets).toEqual(sampleTracePropagationTargets);
});

it('correctly passes tracePropagationTargets to `instrumentOutgoingRequests` in `setupOnce`', () => {
jest.clearAllMocks();
const sampleTracePropagationTargets = ['something'];
createBrowserTracing(true, {
routingInstrumentation: customInstrumentRouting,
tracePropagationTargets: sampleTracePropagationTargets,
});

expect(instrumentOutgoingRequestsMock).toHaveBeenCalledWith({
traceFetch: true,
traceXHR: true,
tracePropagationTargets: ['something'],
});
});
});

describe('beforeNavigate', () => {
it('is called on transaction creation', () => {
const mockBeforeNavigation = jest.fn().mockReturnValue({ name: 'here/is/my/path' });
Expand Down