From 15a36bcd643d37c05f52447adb32d0a149f3d324 Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Tue, 21 Mar 2023 13:17:24 +0100
Subject: [PATCH 01/34] feat(sveltekit): Add performance monitoring to
Sveltekit server handle (#7532)
---
packages/sveltekit/src/server/handle.ts | 103 +++++++
packages/sveltekit/src/server/index.ts | 1 +
packages/sveltekit/test/server/handle.test.ts | 251 ++++++++++++++++++
packages/sveltekit/test/utils.ts | 12 +
4 files changed, 367 insertions(+)
create mode 100644 packages/sveltekit/src/server/handle.ts
create mode 100644 packages/sveltekit/test/server/handle.test.ts
create mode 100644 packages/sveltekit/test/utils.ts
diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts
new file mode 100644
index 000000000000..90dda26dac55
--- /dev/null
+++ b/packages/sveltekit/src/server/handle.ts
@@ -0,0 +1,103 @@
+/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
+import { captureException, getCurrentHub, startTransaction } from '@sentry/node';
+import type { Transaction } from '@sentry/types';
+import {
+ addExceptionMechanism,
+ baggageHeaderToDynamicSamplingContext,
+ extractTraceparentData,
+ isThenable,
+ objectify,
+} from '@sentry/utils';
+import type { Handle } from '@sveltejs/kit';
+import * as domain from 'domain';
+
+function sendErrorToSentry(e: unknown): unknown {
+ // In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
+ // store a seen flag on it.
+ const objectifiedErr = objectify(e);
+
+ captureException(objectifiedErr, scope => {
+ scope.addEventProcessor(event => {
+ addExceptionMechanism(event, {
+ type: 'sveltekit',
+ handled: false,
+ data: {
+ function: 'handle',
+ },
+ });
+ return event;
+ });
+
+ return scope;
+ });
+
+ return objectifiedErr;
+}
+
+/**
+ * A SvelteKit handle function that wraps the request for Sentry error and
+ * performance monitoring.
+ *
+ * Usage:
+ * ```
+ * // src/hooks.server.ts
+ * import { sentryHandle } from '@sentry/sveltekit';
+ *
+ * export const handle = sentryHandle;
+ *
+ * // Optionally use the sequence function to add additional handlers.
+ * // export const handle = sequence(sentryHandle, yourCustomHandle);
+ * ```
+ */
+export const sentryHandle: Handle = ({ event, resolve }) => {
+ return domain.create().bind(() => {
+ let maybePromiseResult;
+
+ const sentryTraceHeader = event.request.headers.get('sentry-trace');
+ const baggageHeader = event.request.headers.get('baggage');
+ const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined;
+ const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader);
+
+ // transaction could be undefined if hub extensions were not added.
+ const transaction: Transaction | undefined = startTransaction({
+ op: 'http.server',
+ name: `${event.request.method} ${event.route.id}`,
+ status: 'ok',
+ ...traceparentData,
+ metadata: {
+ source: 'route',
+ dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
+ },
+ });
+
+ getCurrentHub().getScope()?.setSpan(transaction);
+
+ try {
+ maybePromiseResult = resolve(event);
+ } catch (e) {
+ transaction?.setStatus('internal_error');
+ const sentryError = sendErrorToSentry(e);
+ transaction?.finish();
+ throw sentryError;
+ }
+
+ if (isThenable(maybePromiseResult)) {
+ Promise.resolve(maybePromiseResult).then(
+ response => {
+ transaction?.setHttpStatus(response.status);
+ transaction?.finish();
+ },
+ e => {
+ transaction?.setStatus('internal_error');
+ sendErrorToSentry(e);
+ transaction?.finish();
+ },
+ );
+ } else {
+ transaction?.setHttpStatus(maybePromiseResult.status);
+ transaction?.finish();
+ }
+
+ return maybePromiseResult;
+ })();
+};
diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts
index c7784d870c56..9109f29499d4 100644
--- a/packages/sveltekit/src/server/index.ts
+++ b/packages/sveltekit/src/server/index.ts
@@ -3,3 +3,4 @@ export * from '@sentry/node';
export { init } from './sdk';
export { handleErrorWithSentry } from './handleError';
export { wrapLoadWithSentry } from './load';
+export { sentryHandle } from './handle';
diff --git a/packages/sveltekit/test/server/handle.test.ts b/packages/sveltekit/test/server/handle.test.ts
new file mode 100644
index 000000000000..cf17b56aaa90
--- /dev/null
+++ b/packages/sveltekit/test/server/handle.test.ts
@@ -0,0 +1,251 @@
+import { addTracingExtensions, Hub, makeMain, Scope } from '@sentry/core';
+import { NodeClient } from '@sentry/node';
+import type { Transaction } from '@sentry/types';
+import type { Handle } from '@sveltejs/kit';
+import { vi } from 'vitest';
+
+import { sentryHandle } from '../../src/server/handle';
+import { getDefaultNodeClientOptions } from '../utils';
+
+const mockCaptureException = vi.fn();
+let mockScope = new Scope();
+
+vi.mock('@sentry/node', async () => {
+ const original = (await vi.importActual('@sentry/node')) as any;
+ return {
+ ...original,
+ captureException: (err: unknown, cb: (arg0: unknown) => unknown) => {
+ cb(mockScope);
+ mockCaptureException(err, cb);
+ return original.captureException(err, cb);
+ },
+ };
+});
+
+const mockAddExceptionMechanism = vi.fn();
+
+vi.mock('@sentry/utils', async () => {
+ const original = (await vi.importActual('@sentry/utils')) as any;
+ return {
+ ...original,
+ addExceptionMechanism: (...args: unknown[]) => mockAddExceptionMechanism(...args),
+ };
+});
+
+function mockEvent(override: Record = {}): Parameters[0]['event'] {
+ const event: Parameters[0]['event'] = {
+ cookies: {} as any,
+ fetch: () => Promise.resolve({} as any),
+ getClientAddress: () => '',
+ locals: {},
+ params: { id: '123' },
+ platform: {},
+ request: {
+ method: 'GET',
+ headers: {
+ get: () => null,
+ append: () => {},
+ delete: () => {},
+ forEach: () => {},
+ has: () => false,
+ set: () => {},
+ },
+ } as any,
+ route: { id: '/users/[id]' },
+ setHeaders: () => {},
+ url: new URL('http://localhost:3000/users/123'),
+ isDataRequest: false,
+
+ ...override,
+ };
+
+ return event;
+}
+
+const mockResponse = { status: 200, headers: {}, body: '' } as any;
+
+const enum Type {
+ Sync = 'sync',
+ Async = 'async',
+}
+
+function resolve(type: Type, isError: boolean): Parameters[0]['resolve'] {
+ if (type === Type.Sync) {
+ return (..._args: unknown[]) => {
+ if (isError) {
+ throw new Error(type);
+ }
+
+ return mockResponse;
+ };
+ }
+
+ return (..._args: unknown[]) => {
+ return new Promise((resolve, reject) => {
+ if (isError) {
+ reject(new Error(type));
+ } else {
+ resolve(mockResponse);
+ }
+ });
+ };
+}
+
+let hub: Hub;
+let client: NodeClient;
+
+describe('handleSentry', () => {
+ beforeAll(() => {
+ addTracingExtensions();
+ });
+
+ beforeEach(() => {
+ mockScope = new Scope();
+ const options = getDefaultNodeClientOptions({ tracesSampleRate: 1.0 });
+ client = new NodeClient(options);
+ hub = new Hub(client);
+ makeMain(hub);
+
+ mockCaptureException.mockClear();
+ mockAddExceptionMechanism.mockClear();
+ });
+
+ describe.each([
+ // isSync, isError, expectedResponse
+ [Type.Sync, true, undefined],
+ [Type.Sync, false, mockResponse],
+ [Type.Async, true, undefined],
+ [Type.Async, false, mockResponse],
+ ])('%s resolve with error %s', (type, isError, mockResponse) => {
+ it('should return a response', async () => {
+ let response: any = undefined;
+ try {
+ response = await sentryHandle({ event: mockEvent(), resolve: resolve(type, isError) });
+ } catch (e) {
+ expect(e).toBeInstanceOf(Error);
+ expect(e.message).toEqual(type);
+ }
+
+ expect(response).toEqual(mockResponse);
+ });
+
+ it('creates a transaction', async () => {
+ let ref: any = undefined;
+ client.on('finishTransaction', (transaction: Transaction) => {
+ ref = transaction;
+ });
+
+ try {
+ await sentryHandle({ event: mockEvent(), resolve: resolve(type, isError) });
+ } catch (e) {
+ //
+ }
+
+ expect(ref).toBeDefined();
+
+ expect(ref.name).toEqual('GET /users/[id]');
+ expect(ref.op).toEqual('http.server');
+ expect(ref.status).toEqual(isError ? 'internal_error' : 'ok');
+ expect(ref.metadata.source).toEqual('route');
+
+ expect(ref.endTimestamp).toBeDefined();
+ });
+
+ it('creates a transaction from sentry-trace header', async () => {
+ const event = mockEvent({
+ request: {
+ headers: {
+ get: (key: string) => {
+ if (key === 'sentry-trace') {
+ return '1234567890abcdef1234567890abcdef-1234567890abcdef-1';
+ }
+
+ return null;
+ },
+ },
+ },
+ });
+
+ let ref: any = undefined;
+ client.on('finishTransaction', (transaction: Transaction) => {
+ ref = transaction;
+ });
+
+ try {
+ await sentryHandle({ event, resolve: resolve(type, isError) });
+ } catch (e) {
+ //
+ }
+
+ expect(ref).toBeDefined();
+ expect(ref.traceId).toEqual('1234567890abcdef1234567890abcdef');
+ expect(ref.parentSpanId).toEqual('1234567890abcdef');
+ expect(ref.sampled).toEqual(true);
+ });
+
+ it('creates a transaction with dynamic sampling context from baggage header', async () => {
+ const event = mockEvent({
+ request: {
+ headers: {
+ get: (key: string) => {
+ if (key === 'sentry-trace') {
+ return '1234567890abcdef1234567890abcdef-1234567890abcdef-1';
+ }
+
+ if (key === 'baggage') {
+ return (
+ 'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dogpark,' +
+ 'sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,' +
+ 'sentry-trace_id=1234567890abcdef1234567890abcdef,sentry-sample_rate=1'
+ );
+ }
+
+ return null;
+ },
+ },
+ },
+ });
+
+ let ref: any = undefined;
+ client.on('finishTransaction', (transaction: Transaction) => {
+ ref = transaction;
+ });
+
+ try {
+ await sentryHandle({ event, resolve: resolve(type, isError) });
+ } catch (e) {
+ //
+ }
+
+ expect(ref).toBeDefined();
+ expect(ref.metadata.dynamicSamplingContext).toEqual({
+ environment: 'production',
+ release: '1.0.0',
+ public_key: 'dogsarebadatkeepingsecrets',
+ sample_rate: '1',
+ trace_id: '1234567890abcdef1234567890abcdef',
+ transaction: 'dogpark',
+ user_segment: 'segmentA',
+ });
+ });
+
+ it('send errors to Sentry', async () => {
+ const addEventProcessorSpy = vi.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => {
+ void callback({}, { event_id: 'fake-event-id' });
+ return mockScope;
+ });
+
+ try {
+ await sentryHandle({ event: mockEvent(), resolve: resolve(type, isError) });
+ } catch (e) {
+ expect(mockCaptureException).toBeCalledTimes(1);
+ expect(addEventProcessorSpy).toBeCalledTimes(1);
+ expect(mockAddExceptionMechanism).toBeCalledTimes(1);
+ expect(mockAddExceptionMechanism).toBeCalledWith(
+ {},
+ { handled: false, type: 'sveltekit', data: { function: 'handle' } },
+ );
+ }
+ });
+ });
+});
diff --git a/packages/sveltekit/test/utils.ts b/packages/sveltekit/test/utils.ts
new file mode 100644
index 000000000000..993a6bd8823d
--- /dev/null
+++ b/packages/sveltekit/test/utils.ts
@@ -0,0 +1,12 @@
+import { createTransport } from '@sentry/core';
+import type { ClientOptions } from '@sentry/types';
+import { resolvedSyncPromise } from '@sentry/utils';
+
+export function getDefaultNodeClientOptions(options: Partial = {}): ClientOptions {
+ return {
+ integrations: [],
+ transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})),
+ stackParser: () => [],
+ ...options,
+ };
+}
From 6426b58c7bf387ff922382c2577631002c1aa977 Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Tue, 21 Mar 2023 15:19:20 +0100
Subject: [PATCH 02/34] feat(hub): Make scope always defined on the hub (#7551)
---
packages/core/src/hub.ts | 56 +++++++++++++++++-----------------------
1 file changed, 23 insertions(+), 33 deletions(-)
diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts
index 203241a35389..734b4c28711a 100644
--- a/packages/core/src/hub.ts
+++ b/packages/core/src/hub.ts
@@ -56,7 +56,7 @@ const DEFAULT_BREADCRUMBS = 100;
*/
export interface Layer {
client?: Client;
- scope?: Scope;
+ scope: Scope;
}
/**
@@ -87,7 +87,7 @@ export interface Carrier {
*/
export class Hub implements HubInterface {
/** Is a {@link Layer}[] containing the client and scope */
- private readonly _stack: Layer[] = [{}];
+ private readonly _stack: Layer[];
/** Contains the last event id of a captured event. */
private _lastEventId?: string;
@@ -101,7 +101,7 @@ export class Hub implements HubInterface {
* @param version number, higher number means higher priority.
*/
public constructor(client?: Client, scope: Scope = new Scope(), private readonly _version: number = API_VERSION) {
- this.getStackTop().scope = scope;
+ this._stack = [{ scope }];
if (client) {
this.bindClient(client);
}
@@ -166,7 +166,7 @@ export class Hub implements HubInterface {
}
/** Returns the scope of the top stack. */
- public getScope(): Scope | undefined {
+ public getScope(): Scope {
return this.getStackTop().scope;
}
@@ -256,7 +256,7 @@ export class Hub implements HubInterface {
public addBreadcrumb(breadcrumb: Breadcrumb, hint?: BreadcrumbHint): void {
const { scope, client } = this.getStackTop();
- if (!scope || !client) return;
+ if (!client) return;
const { beforeBreadcrumb = null, maxBreadcrumbs = DEFAULT_BREADCRUMBS } =
(client.getOptions && client.getOptions()) || {};
@@ -282,40 +282,35 @@ export class Hub implements HubInterface {
* @inheritDoc
*/
public setUser(user: User | null): void {
- const scope = this.getScope();
- if (scope) scope.setUser(user);
+ this.getScope().setUser(user);
}
/**
* @inheritDoc
*/
public setTags(tags: { [key: string]: Primitive }): void {
- const scope = this.getScope();
- if (scope) scope.setTags(tags);
+ this.getScope().setTags(tags);
}
/**
* @inheritDoc
*/
public setExtras(extras: Extras): void {
- const scope = this.getScope();
- if (scope) scope.setExtras(extras);
+ this.getScope().setExtras(extras);
}
/**
* @inheritDoc
*/
public setTag(key: string, value: Primitive): void {
- const scope = this.getScope();
- if (scope) scope.setTag(key, value);
+ this.getScope().setTag(key, value);
}
/**
* @inheritDoc
*/
public setExtra(key: string, extra: Extra): void {
- const scope = this.getScope();
- if (scope) scope.setExtra(key, extra);
+ this.getScope().setExtra(key, extra);
}
/**
@@ -323,8 +318,7 @@ export class Hub implements HubInterface {
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public setContext(name: string, context: { [key: string]: any } | null): void {
- const scope = this.getScope();
- if (scope) scope.setContext(name, context);
+ this.getScope().setContext(name, context);
}
/**
@@ -395,17 +389,15 @@ export class Hub implements HubInterface {
*/
public endSession(): void {
const layer = this.getStackTop();
- const scope = layer && layer.scope;
- const session = scope && scope.getSession();
+ const scope = layer.scope;
+ const session = scope.getSession();
if (session) {
closeSession(session);
}
this._sendSessionUpdate();
// the session is over; take it off of the scope
- if (scope) {
- scope.setSession();
- }
+ scope.setSession();
}
/**
@@ -426,17 +418,15 @@ export class Hub implements HubInterface {
...context,
});
- if (scope) {
- // End existing session if there's one
- const currentSession = scope.getSession && scope.getSession();
- if (currentSession && currentSession.status === 'ok') {
- updateSession(currentSession, { status: 'exited' });
- }
- this.endSession();
-
- // Afterwards we set the new session on the scope
- scope.setSession(session);
+ // End existing session if there's one
+ const currentSession = scope.getSession && scope.getSession();
+ if (currentSession && currentSession.status === 'ok') {
+ updateSession(currentSession, { status: 'exited' });
}
+ this.endSession();
+
+ // Afterwards we set the new session on the scope
+ scope.setSession(session);
return session;
}
@@ -472,7 +462,7 @@ export class Hub implements HubInterface {
* @param method The method to call on the client.
* @param args Arguments to pass to the client function.
*/
- private _withClient(callback: (client: Client, scope: Scope | undefined) => void): void {
+ private _withClient(callback: (client: Client, scope: Scope) => void): void {
const { scope, client } = this.getStackTop();
if (client) {
callback(client, scope);
From 5c5ac2ceb65aee04f50fd0b5a1eacc01a79c65d9 Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Tue, 21 Mar 2023 15:46:08 +0100
Subject: [PATCH 03/34] ref(browser): Remove `sendBeacon` API usage (#7552)
---
packages/browser/src/client.ts | 23 +++--------------------
1 file changed, 3 insertions(+), 20 deletions(-)
diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts
index 8cafc77a68c3..1d0bd091cf19 100644
--- a/packages/browser/src/client.ts
+++ b/packages/browser/src/client.ts
@@ -1,5 +1,5 @@
import type { Scope } from '@sentry/core';
-import { BaseClient, getEnvelopeEndpointWithUrlEncodedAuth, SDK_VERSION } from '@sentry/core';
+import { BaseClient, SDK_VERSION } from '@sentry/core';
import type {
BrowserClientReplayOptions,
ClientOptions,
@@ -9,7 +9,7 @@ import type {
Severity,
SeverityLevel,
} from '@sentry/types';
-import { createClientReportEnvelope, dsnToString, getSDKSource, logger, serializeEnvelope } from '@sentry/utils';
+import { createClientReportEnvelope, dsnToString, getSDKSource, logger } from '@sentry/utils';
import { eventFromException, eventFromMessage } from './eventbuilder';
import { WINDOW } from './helpers';
@@ -132,24 +132,7 @@ export class BrowserClient extends BaseClient {
__DEBUG_BUILD__ && logger.log('Sending outcomes:', outcomes);
- const url = getEnvelopeEndpointWithUrlEncodedAuth(this._dsn, this._options);
const envelope = createClientReportEnvelope(outcomes, this._options.tunnel && dsnToString(this._dsn));
-
- try {
- const isRealNavigator = Object.prototype.toString.call(WINDOW && WINDOW.navigator) === '[object Navigator]';
- const hasSendBeacon = isRealNavigator && typeof WINDOW.navigator.sendBeacon === 'function';
- // Make sure beacon is not used if user configures custom transport options
- if (hasSendBeacon && !this._options.transportOptions) {
- // Prevent illegal invocations - https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch
- const sendBeacon = WINDOW.navigator.sendBeacon.bind(WINDOW.navigator);
- sendBeacon(url, serializeEnvelope(envelope));
- } else {
- // If beacon is not supported or if they are using the tunnel option
- // use our regular transport to send client reports to Sentry.
- void this._sendEnvelope(envelope);
- }
- } catch (e) {
- __DEBUG_BUILD__ && logger.error(e);
- }
+ void this._sendEnvelope(envelope);
}
}
From 7b9198f24fa44271eda99c6f607e36a940b93463 Mon Sep 17 00:00:00 2001
From: Francesco Novy
Date: Tue, 21 Mar 2023 16:53:01 +0100
Subject: [PATCH 04/34] fix(browser): Ensure keepalive flag is correctly set
for parallel requests (#7553)
We noticed that sometimes request would remain in a seemingly pending state.
After some investigation, we found out that the limit of 64kb for keepalive-enabled fetch requests is not per request but for all parallel requests running at the same time.
This fixes this by keeping track of how large the pending body sizes are, plus the # of pending requests, and setting keepalive accordingly.
---
packages/browser/src/transports/fetch.ts | 37 +++++++++----
.../test/unit/transports/fetch.test.ts | 55 +++++++++++++++++++
2 files changed, 81 insertions(+), 11 deletions(-)
diff --git a/packages/browser/src/transports/fetch.ts b/packages/browser/src/transports/fetch.ts
index 83b75c82ba39..e996b6f8277a 100644
--- a/packages/browser/src/transports/fetch.ts
+++ b/packages/browser/src/transports/fetch.ts
@@ -13,7 +13,14 @@ export function makeFetchTransport(
options: BrowserTransportOptions,
nativeFetch: FetchImpl = getNativeFetchImplementation(),
): Transport {
+ let pendingBodySize = 0;
+ let pendingCount = 0;
+
function makeRequest(request: TransportRequest): PromiseLike {
+ const requestSize = request.body.length;
+ pendingBodySize += requestSize;
+ pendingCount++;
+
const requestOptions: RequestInit = {
body: request.body,
method: 'POST',
@@ -25,23 +32,31 @@ export function makeFetchTransport(
// frequently sending events right before the user is switching pages (eg. whenfinishing navigation transactions).
// Gotchas:
// - `keepalive` isn't supported by Firefox
- // - As per spec (https://fetch.spec.whatwg.org/#http-network-or-cache-fetch), a request with `keepalive: true`
- // and a content length of > 64 kibibytes returns a network error. We will therefore only activate the flag when
- // we're below that limit.
- keepalive: request.body.length <= 65536,
+ // - As per spec (https://fetch.spec.whatwg.org/#http-network-or-cache-fetch):
+ // If the sum of contentLength and inflightKeepaliveBytes is greater than 64 kibibytes, then return a network error.
+ // We will therefore only activate the flag when we're below that limit.
+ // There is also a limit of requests that can be open at the same time, so we also limit this to 15
+ // See https://github.com/getsentry/sentry-javascript/pull/7553 for details
+ keepalive: pendingBodySize <= 60_000 && pendingCount < 15,
...options.fetchOptions,
};
try {
- return nativeFetch(options.url, requestOptions).then(response => ({
- statusCode: response.status,
- headers: {
- 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
- 'retry-after': response.headers.get('Retry-After'),
- },
- }));
+ return nativeFetch(options.url, requestOptions).then(response => {
+ pendingBodySize -= requestSize;
+ pendingCount--;
+ return {
+ statusCode: response.status,
+ headers: {
+ 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
+ 'retry-after': response.headers.get('Retry-After'),
+ },
+ };
+ });
} catch (e) {
clearCachedFetchImplementation();
+ pendingBodySize -= requestSize;
+ pendingCount--;
return rejectedSyncPromise(e);
}
}
diff --git a/packages/browser/test/unit/transports/fetch.test.ts b/packages/browser/test/unit/transports/fetch.test.ts
index 53ed5c34e21b..e6b58eeaa110 100644
--- a/packages/browser/test/unit/transports/fetch.test.ts
+++ b/packages/browser/test/unit/transports/fetch.test.ts
@@ -16,6 +16,11 @@ const ERROR_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4b
[{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem,
]);
+const LARGE_ERROR_ENVELOPE = createEnvelope(
+ { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' },
+ [[{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', message: 'x'.repeat(10 * 900) }] as EventItem],
+);
+
class Headers {
headers: { [key: string]: string } = {};
get(key: string) {
@@ -107,4 +112,54 @@ describe('NewFetchTransport', () => {
await expect(() => transport.send(ERROR_ENVELOPE)).not.toThrow();
expect(mockFetch).toHaveBeenCalledTimes(1);
});
+
+ it('correctly sets keepalive flag', async () => {
+ const mockFetch = jest.fn(() =>
+ Promise.resolve({
+ headers: new Headers(),
+ status: 200,
+ text: () => Promise.resolve({}),
+ }),
+ ) as unknown as FetchImpl;
+
+ const REQUEST_OPTIONS: RequestInit = {
+ referrerPolicy: 'strict-origin',
+ referrer: 'http://example.org',
+ };
+
+ const transport = makeFetchTransport(
+ { ...DEFAULT_FETCH_TRANSPORT_OPTIONS, fetchOptions: REQUEST_OPTIONS },
+ mockFetch,
+ );
+
+ const promises: PromiseLike[] = [];
+ for (let i = 0; i < 30; i++) {
+ promises.push(transport.send(LARGE_ERROR_ENVELOPE));
+ }
+
+ await Promise.all(promises);
+
+ for (let i = 1; i <= 30; i++) {
+ // After 7 requests, we hit the total limit of >64kb of size
+ // Starting there, keepalive should be false
+ const keepalive = i < 7;
+ expect(mockFetch).toHaveBeenNthCalledWith(i, expect.any(String), expect.objectContaining({ keepalive }));
+ }
+
+ (mockFetch as jest.Mock).mockClear();
+
+ // Limit resets when requests have resolved
+ // Now try based on # of pending requests
+ const promises2 = [];
+ for (let i = 0; i < 20; i++) {
+ promises2.push(transport.send(ERROR_ENVELOPE));
+ }
+
+ await Promise.all(promises2);
+
+ for (let i = 1; i <= 20; i++) {
+ const keepalive = i < 15;
+ expect(mockFetch).toHaveBeenNthCalledWith(i, expect.any(String), expect.objectContaining({ keepalive }));
+ }
+ });
});
From 72dca3e1bfe4e5756c44ef1e4f7b139c0ec6d2c5 Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Tue, 21 Mar 2023 16:54:06 +0100
Subject: [PATCH 05/34] chore(otel): Update docs for using propagator (#7548)
---
packages/opentelemetry-node/README.md | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/packages/opentelemetry-node/README.md b/packages/opentelemetry-node/README.md
index 551684a75c26..b75ca0ab0ef8 100644
--- a/packages/opentelemetry-node/README.md
+++ b/packages/opentelemetry-node/README.md
@@ -66,10 +66,9 @@ const sdk = new opentelemetry.NodeSDK({
// Sentry config
spanProcessor: new SentrySpanProcessor(),
+ textMapPropagator: new SentryPropagator(),
});
-otelApi.propagation.setGlobalPropagator(new SentryPropagator());
-
sdk.start();
```
From 2738b5ff3fca3816c506eb1736a4c0a055c0fcf7 Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Tue, 21 Mar 2023 18:50:24 +0100
Subject: [PATCH 06/34] ref(core): Remove guards around scope usage (#7554)
---
packages/core/src/hub.ts | 4 ++--
packages/core/src/sdk.ts | 4 +---
packages/core/src/sessionflusher.ts | 6 ++----
packages/core/src/tracing/hubextensions.ts | 14 ++++++--------
packages/core/src/tracing/idletransaction.ts | 7 ++-----
packages/core/src/tracing/transaction.ts | 3 +--
packages/core/src/tracing/utils.ts | 2 +-
7 files changed, 15 insertions(+), 25 deletions(-)
diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts
index 734b4c28711a..f67ae3773325 100644
--- a/packages/core/src/hub.ts
+++ b/packages/core/src/hub.ts
@@ -326,7 +326,7 @@ export class Hub implements HubInterface {
*/
public configureScope(callback: (scope: Scope) => void): void {
const { scope, client } = this.getStackTop();
- if (scope && client) {
+ if (client) {
callback(scope);
}
}
@@ -413,7 +413,7 @@ export class Hub implements HubInterface {
const session = makeSession({
release,
environment,
- ...(scope && { user: scope.getUser() }),
+ user: scope.getUser(),
...(userAgent && { userAgent }),
...context,
});
diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts
index 7d2df2115ddd..5dc58a5fc944 100644
--- a/packages/core/src/sdk.ts
+++ b/packages/core/src/sdk.ts
@@ -28,9 +28,7 @@ export function initAndBind(
}
const hub = getCurrentHub();
const scope = hub.getScope();
- if (scope) {
- scope.update(options.initialScope);
- }
+ scope.update(options.initialScope);
const client = new clientClass(options);
hub.bindClient(client);
diff --git a/packages/core/src/sessionflusher.ts b/packages/core/src/sessionflusher.ts
index 9b4579ade486..4ef196819504 100644
--- a/packages/core/src/sessionflusher.ts
+++ b/packages/core/src/sessionflusher.ts
@@ -72,15 +72,13 @@ export class SessionFlusher implements SessionFlusherLike {
return;
}
const scope = getCurrentHub().getScope();
- const requestSession = scope && scope.getRequestSession();
+ const requestSession = scope.getRequestSession();
if (requestSession && requestSession.status) {
this._incrementSessionStatusCount(requestSession.status, new Date());
// This is not entirely necessarily but is added as a safe guard to indicate the bounds of a request and so in
// case captureRequestSession is called more than once to prevent double count
- if (scope) {
- scope.setRequestSession(undefined);
- }
+ scope.setRequestSession(undefined);
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
}
}
diff --git a/packages/core/src/tracing/hubextensions.ts b/packages/core/src/tracing/hubextensions.ts
index b5518f95e04b..ad6858d41e4a 100644
--- a/packages/core/src/tracing/hubextensions.ts
+++ b/packages/core/src/tracing/hubextensions.ts
@@ -11,15 +11,13 @@ import { Transaction } from './transaction';
/** Returns all trace headers that are currently on the top scope. */
function traceHeaders(this: Hub): { [key: string]: string } {
const scope = this.getScope();
- if (scope) {
- const span = scope.getSpan();
- if (span) {
- return {
+ const span = scope.getSpan();
+
+ return span
+ ? {
'sentry-trace': span.toTraceparent(),
- };
- }
- }
- return {};
+ }
+ : {};
}
/**
diff --git a/packages/core/src/tracing/idletransaction.ts b/packages/core/src/tracing/idletransaction.ts
index f4604a312519..77b7d5317da1 100644
--- a/packages/core/src/tracing/idletransaction.ts
+++ b/packages/core/src/tracing/idletransaction.ts
@@ -346,10 +346,7 @@ export class IdleTransaction extends Transaction {
*/
function clearActiveTransaction(hub: Hub): void {
const scope = hub.getScope();
- if (scope) {
- const transaction = scope.getTransaction();
- if (transaction) {
- scope.setSpan(undefined);
- }
+ if (scope.getTransaction()) {
+ scope.setSpan(undefined);
}
}
diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts
index eba498b7e654..b1e6e74195be 100644
--- a/packages/core/src/tracing/transaction.ts
+++ b/packages/core/src/tracing/transaction.ts
@@ -256,8 +256,7 @@ export class Transaction extends SpanClass implements TransactionInterface {
const maybeSampleRate = this.metadata.sampleRate;
const sample_rate = maybeSampleRate !== undefined ? maybeSampleRate.toString() : undefined;
- const scope = hub.getScope();
- const { segment: user_segment } = (scope && scope.getUser()) || {};
+ const { segment: user_segment } = hub.getScope().getUser() || {};
const source = this.metadata.source;
diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts
index 5dc64f98d400..624fff153270 100644
--- a/packages/core/src/tracing/utils.ts
+++ b/packages/core/src/tracing/utils.ts
@@ -21,7 +21,7 @@ export { TRACEPARENT_REGEXP, extractTraceparentData } from '@sentry/utils';
export function getActiveTransaction(maybeHub?: Hub): T | undefined {
const hub = maybeHub || getCurrentHub();
const scope = hub.getScope();
- return scope && (scope.getTransaction() as T | undefined);
+ return scope.getTransaction() as T | undefined;
}
// so it can be used in manual instrumentation without necessitating a hard dependency on @sentry/utils
From 95b0a6cdaa9d6f05e9775021fca0bb31d22f4dc4 Mon Sep 17 00:00:00 2001
From: Tim Fish
Date: Tue, 21 Mar 2023 18:56:49 +0000
Subject: [PATCH 07/34] feat(node): Export tracing from `@sentry/node` (#7503)
---
packages/node-integration-tests/package.json | 3 +-
.../suites/express/tracing/server.ts | 3 +-
.../tracing-new/apollo-graphql/scenario.ts | 43 ++++++++++
.../suites/tracing-new/apollo-graphql/test.ts | 35 ++++++++
.../auto-instrument/mongodb/scenario.ts | 46 ++++++++++
.../auto-instrument/mongodb/test.ts | 85 +++++++++++++++++++
.../auto-instrument/mysql/scenario.ts | 36 ++++++++
.../tracing-new/auto-instrument/mysql/test.ts | 23 +++++
.../auto-instrument/pg/scenario.ts | 25 ++++++
.../tracing-new/auto-instrument/pg/test.ts | 54 ++++++++++++
.../tracing-new/prisma-orm/docker-compose.yml | 13 +++
.../tracing-new/prisma-orm/package.json | 22 +++++
.../prisma/migrations/migration_lock.toml | 3 +
.../migrations/sentry_test/migration.sql | 12 +++
.../prisma-orm/prisma/schema.prisma | 15 ++++
.../suites/tracing-new/prisma-orm/scenario.ts | 47 ++++++++++
.../suites/tracing-new/prisma-orm/setup.ts | 16 ++++
.../suites/tracing-new/prisma-orm/test.ts | 17 ++++
.../suites/tracing-new/prisma-orm/yarn.lock | 27 ++++++
.../tracePropagationTargets/scenario.ts | 26 ++++++
.../tracePropagationTargets/test.ts | 42 +++++++++
packages/node/package.json | 1 +
packages/node/src/client.ts | 5 +-
packages/node/src/index.ts | 3 +
packages/node/src/tracing/index.ts | 24 ++++++
packages/node/src/tracing/integrations.ts | 1 +
packages/tracing-internal/src/index.ts | 11 ++-
.../src/node/integrations/index.ts | 1 +
.../src/node/integrations/lazy.ts | 47 ++++++++++
29 files changed, 681 insertions(+), 5 deletions(-)
create mode 100644 packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts
create mode 100644 packages/node-integration-tests/suites/tracing-new/apollo-graphql/test.ts
create mode 100644 packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts
create mode 100644 packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/test.ts
create mode 100644 packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/scenario.ts
create mode 100644 packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/test.ts
create mode 100644 packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts
create mode 100644 packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/test.ts
create mode 100644 packages/node-integration-tests/suites/tracing-new/prisma-orm/docker-compose.yml
create mode 100644 packages/node-integration-tests/suites/tracing-new/prisma-orm/package.json
create mode 100644 packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/migrations/migration_lock.toml
create mode 100644 packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/migrations/sentry_test/migration.sql
create mode 100644 packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/schema.prisma
create mode 100644 packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts
create mode 100755 packages/node-integration-tests/suites/tracing-new/prisma-orm/setup.ts
create mode 100644 packages/node-integration-tests/suites/tracing-new/prisma-orm/test.ts
create mode 100644 packages/node-integration-tests/suites/tracing-new/prisma-orm/yarn.lock
create mode 100644 packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts
create mode 100644 packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/test.ts
create mode 100644 packages/node/src/tracing/index.ts
create mode 100644 packages/node/src/tracing/integrations.ts
create mode 100644 packages/tracing-internal/src/node/integrations/lazy.ts
diff --git a/packages/node-integration-tests/package.json b/packages/node-integration-tests/package.json
index f1661102a963..e63a6e940dca 100644
--- a/packages/node-integration-tests/package.json
+++ b/packages/node-integration-tests/package.json
@@ -9,6 +9,7 @@
"scripts": {
"clean": "rimraf -g **/node_modules",
"prisma:init": "(cd suites/tracing/prisma-orm && ts-node ./setup.ts)",
+ "prisma:init:new": "(cd suites/tracing-new/prisma-orm && ts-node ./setup.ts)",
"lint": "run-s lint:prettier lint:eslint",
"lint:eslint": "eslint . --format stylish",
"lint:prettier": "prettier --check \"{suites,utils}/**/*.ts\"",
@@ -16,7 +17,7 @@
"fix:eslint": "eslint . --format stylish --fix",
"fix:prettier": "prettier --write \"{suites,utils}/**/*.ts\"",
"type-check": "tsc",
- "pretest": "run-s --silent prisma:init",
+ "pretest": "run-s --silent prisma:init prisma:init:new",
"test": "ts-node ./utils/run-tests.ts",
"test:watch": "yarn test --watch"
},
diff --git a/packages/node-integration-tests/suites/express/tracing/server.ts b/packages/node-integration-tests/suites/express/tracing/server.ts
index faf5a50f95ed..e857621ad22e 100644
--- a/packages/node-integration-tests/suites/express/tracing/server.ts
+++ b/packages/node-integration-tests/suites/express/tracing/server.ts
@@ -1,5 +1,4 @@
import * as Sentry from '@sentry/node';
-import * as Tracing from '@sentry/tracing';
import cors from 'cors';
import express from 'express';
@@ -8,7 +7,7 @@ const app = express();
Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
- integrations: [new Sentry.Integrations.Http({ tracing: true }), new Tracing.Integrations.Express({ app })],
+ integrations: [new Sentry.Integrations.Http({ tracing: true }), new Sentry.Integrations.Express({ app })],
tracesSampleRate: 1.0,
});
diff --git a/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts b/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts
new file mode 100644
index 000000000000..5bd8aa815cbe
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts
@@ -0,0 +1,43 @@
+import * as Sentry from '@sentry/node';
+import { ApolloServer, gql } from 'apollo-server';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ integrations: [new Sentry.Integrations.GraphQL(), new Sentry.Integrations.Apollo()],
+});
+
+const typeDefs = gql`
+ type Query {
+ hello: String
+ }
+`;
+
+const resolvers = {
+ Query: {
+ hello: () => {
+ return 'Hello world!';
+ },
+ },
+};
+
+const server = new ApolloServer({
+ typeDefs,
+ resolvers,
+});
+
+const transaction = Sentry.startTransaction({ name: 'test_transaction', op: 'transaction' });
+
+Sentry.configureScope(scope => {
+ scope.setSpan(transaction);
+});
+
+void (async () => {
+ // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation
+ await server.executeOperation({
+ query: '{hello}',
+ });
+
+ transaction.finish();
+})();
diff --git a/packages/node-integration-tests/suites/tracing-new/apollo-graphql/test.ts b/packages/node-integration-tests/suites/tracing-new/apollo-graphql/test.ts
new file mode 100644
index 000000000000..128f8a2f164b
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/apollo-graphql/test.ts
@@ -0,0 +1,35 @@
+import { assertSentryTransaction, conditionalTest, TestEnv } from '../../../utils';
+
+// Node 10 is not supported by `graphql-js`
+// Ref: https://github.com/graphql/graphql-js/blob/main/package.json
+conditionalTest({ min: 12 })('GraphQL/Apollo Tests', () => {
+ test('should instrument GraphQL and Apollo Server.', async () => {
+ const env = await TestEnv.init(__dirname);
+ const envelope = await env.getEnvelopeRequest({ envelopeType: 'transaction' });
+
+ expect(envelope).toHaveLength(3);
+
+ const transaction = envelope[2];
+ const parentSpanId = (transaction as any)?.contexts?.trace?.span_id;
+ const graphqlSpanId = (transaction as any)?.spans?.[0].span_id;
+
+ expect(parentSpanId).toBeDefined();
+ expect(graphqlSpanId).toBeDefined();
+
+ assertSentryTransaction(transaction, {
+ transaction: 'test_transaction',
+ spans: [
+ {
+ description: 'execute',
+ op: 'graphql.execute',
+ parent_span_id: parentSpanId,
+ },
+ {
+ description: 'Query.hello',
+ op: 'graphql.resolve',
+ parent_span_id: graphqlSpanId,
+ },
+ ],
+ });
+ });
+});
diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts
new file mode 100644
index 000000000000..31d7356765e9
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts
@@ -0,0 +1,46 @@
+import * as Sentry from '@sentry/node';
+import { MongoClient } from 'mongodb';
+
+// suppress logging of the mongo download
+global.console.log = () => null;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ integrations: [...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations()],
+});
+
+const client = new MongoClient(process.env.MONGO_URL || '', {
+ useUnifiedTopology: true,
+});
+
+async function run(): Promise {
+ const transaction = Sentry.startTransaction({
+ name: 'Test Transaction',
+ op: 'transaction',
+ });
+
+ Sentry.configureScope(scope => {
+ scope.setSpan(transaction);
+ });
+
+ try {
+ await client.connect();
+
+ const database = client.db('admin');
+ const collection = database.collection('movies');
+
+ await collection.insertOne({ title: 'Rick and Morty' });
+ await collection.findOne({ title: 'Back to the Future' });
+ await collection.updateOne({ title: 'Back to the Future' }, { $set: { title: 'South Park' } });
+ await collection.findOne({ title: 'South Park' });
+
+ await collection.find({ title: 'South Park' }).toArray();
+ } finally {
+ if (transaction) transaction.finish();
+ await client.close();
+ }
+}
+
+void run();
diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/test.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/test.ts
new file mode 100644
index 000000000000..5664aac9422b
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/test.ts
@@ -0,0 +1,85 @@
+import { MongoMemoryServer } from 'mongodb-memory-server-global';
+
+import { assertSentryTransaction, conditionalTest, TestEnv } from '../../../../utils';
+
+// This test can take longer.
+jest.setTimeout(15000);
+
+conditionalTest({ min: 12 })('MongoDB Test', () => {
+ let mongoServer: MongoMemoryServer;
+
+ beforeAll(async () => {
+ mongoServer = await MongoMemoryServer.create();
+ process.env.MONGO_URL = mongoServer.getUri();
+ }, 10000);
+
+ afterAll(async () => {
+ if (mongoServer) {
+ await mongoServer.stop();
+ }
+ });
+
+ test('should auto-instrument `mongodb` package.', async () => {
+ const env = await TestEnv.init(__dirname);
+ const envelope = await env.getEnvelopeRequest({ envelopeType: 'transaction' });
+
+ expect(envelope).toHaveLength(3);
+
+ assertSentryTransaction(envelope[2], {
+ transaction: 'Test Transaction',
+ spans: [
+ {
+ data: {
+ collectionName: 'movies',
+ dbName: 'admin',
+ namespace: 'admin.movies',
+ doc: '{"title":"Rick and Morty"}',
+ },
+ description: 'insertOne',
+ op: 'db',
+ },
+ {
+ data: {
+ collectionName: 'movies',
+ dbName: 'admin',
+ namespace: 'admin.movies',
+ query: '{"title":"Back to the Future"}',
+ },
+ description: 'findOne',
+ op: 'db',
+ },
+ {
+ data: {
+ collectionName: 'movies',
+ dbName: 'admin',
+ namespace: 'admin.movies',
+ filter: '{"title":"Back to the Future"}',
+ update: '{"$set":{"title":"South Park"}}',
+ },
+ description: 'updateOne',
+ op: 'db',
+ },
+ {
+ data: {
+ collectionName: 'movies',
+ dbName: 'admin',
+ namespace: 'admin.movies',
+ query: '{"title":"South Park"}',
+ },
+ description: 'findOne',
+ op: 'db',
+ },
+ {
+ data: {
+ collectionName: 'movies',
+ dbName: 'admin',
+ namespace: 'admin.movies',
+ query: '{"title":"South Park"}',
+ },
+ description: 'find',
+ op: 'db',
+ },
+ ],
+ });
+ });
+});
diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/scenario.ts
new file mode 100644
index 000000000000..0f576cb793aa
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/scenario.ts
@@ -0,0 +1,36 @@
+import * as Sentry from '@sentry/node';
+import mysql from 'mysql';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ integrations: [...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations()],
+});
+
+const connection = mysql.createConnection({
+ user: 'root',
+ password: 'docker',
+});
+
+connection.connect(function (err: unknown) {
+ if (err) {
+ return;
+ }
+});
+
+const transaction = Sentry.startTransaction({
+ op: 'transaction',
+ name: 'Test Transaction',
+});
+
+Sentry.configureScope(scope => {
+ scope.setSpan(transaction);
+});
+
+connection.query('SELECT 1 + 1 AS solution', function () {
+ connection.query('SELECT NOW()', ['1', '2'], () => {
+ if (transaction) transaction.finish();
+ connection.end();
+ });
+});
diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/test.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/test.ts
new file mode 100644
index 000000000000..3b96f2cafec0
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/test.ts
@@ -0,0 +1,23 @@
+import { assertSentryTransaction, TestEnv } from '../../../../utils';
+
+test('should auto-instrument `mysql` package.', async () => {
+ const env = await TestEnv.init(__dirname);
+ const envelope = await env.getEnvelopeRequest({ envelopeType: 'transaction' });
+
+ expect(envelope).toHaveLength(3);
+
+ assertSentryTransaction(envelope[2], {
+ transaction: 'Test Transaction',
+ spans: [
+ {
+ description: 'SELECT 1 + 1 AS solution',
+ op: 'db',
+ },
+
+ {
+ description: 'SELECT NOW()',
+ op: 'db',
+ },
+ ],
+ });
+});
diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts
new file mode 100644
index 000000000000..a7859fd562a3
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts
@@ -0,0 +1,25 @@
+import * as Sentry from '@sentry/node';
+import pg from 'pg';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ integrations: [...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations()],
+});
+
+const transaction = Sentry.startTransaction({
+ op: 'transaction',
+ name: 'Test Transaction',
+});
+
+Sentry.configureScope(scope => {
+ scope.setSpan(transaction);
+});
+
+const client = new pg.Client();
+client.query('SELECT * FROM foo where bar ilike "baz%"', ['a', 'b'], () =>
+ client.query('SELECT * FROM bazz', () => {
+ client.query('SELECT NOW()', () => transaction.finish());
+ }),
+);
diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/test.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/test.ts
new file mode 100644
index 000000000000..edfa67cee9d7
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/test.ts
@@ -0,0 +1,54 @@
+import { assertSentryTransaction, TestEnv } from '../../../../utils';
+
+class PgClient {
+ // https://node-postgres.com/api/client#clientquery
+ public query(_text: unknown, values: unknown, callback?: () => void) {
+ if (typeof callback === 'function') {
+ callback();
+ return;
+ }
+
+ if (typeof values === 'function') {
+ values();
+ return;
+ }
+
+ return Promise.resolve();
+ }
+}
+
+beforeAll(() => {
+ jest.mock('pg', () => {
+ return {
+ Client: PgClient,
+ native: {
+ Client: PgClient,
+ },
+ };
+ });
+});
+
+test('should auto-instrument `pg` package.', async () => {
+ const env = await TestEnv.init(__dirname);
+ const envelope = await env.getEnvelopeRequest({ envelopeType: 'transaction' });
+
+ expect(envelope).toHaveLength(3);
+
+ assertSentryTransaction(envelope[2], {
+ transaction: 'Test Transaction',
+ spans: [
+ {
+ description: 'SELECT * FROM foo where bar ilike "baz%"',
+ op: 'db',
+ },
+ {
+ description: 'SELECT * FROM bazz',
+ op: 'db',
+ },
+ {
+ description: 'SELECT NOW()',
+ op: 'db',
+ },
+ ],
+ });
+});
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/docker-compose.yml b/packages/node-integration-tests/suites/tracing-new/prisma-orm/docker-compose.yml
new file mode 100644
index 000000000000..45caa4bb3179
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/docker-compose.yml
@@ -0,0 +1,13 @@
+version: '3.9'
+
+services:
+ db:
+ image: postgres:13
+ restart: always
+ container_name: integration-tests-prisma
+ ports:
+ - '5433:5432'
+ environment:
+ POSTGRES_USER: prisma
+ POSTGRES_PASSWORD: prisma
+ POSTGRES_DB: tests
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/package.json b/packages/node-integration-tests/suites/tracing-new/prisma-orm/package.json
new file mode 100644
index 000000000000..f8b24d7d0465
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "sentry-prisma-test",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "engines": {
+ "node": ">=12"
+ },
+ "scripts": {
+ "db-up": "docker-compose up -d",
+ "generate": "prisma generate",
+ "migrate": "prisma migrate dev -n sentry-test",
+ "setup": "run-s --silent db-up generate migrate"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@prisma/client": "3.12.0",
+ "prisma": "^3.12.0"
+ }
+}
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/migrations/migration_lock.toml b/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/migrations/migration_lock.toml
new file mode 100644
index 000000000000..fbffa92c2bb7
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (i.e. Git)
+provider = "postgresql"
\ No newline at end of file
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/migrations/sentry_test/migration.sql b/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/migrations/sentry_test/migration.sql
new file mode 100644
index 000000000000..8619aaceb2b0
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/migrations/sentry_test/migration.sql
@@ -0,0 +1,12 @@
+-- CreateTable
+CREATE TABLE "User" (
+ "id" SERIAL NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "email" TEXT NOT NULL,
+ "name" TEXT,
+
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/schema.prisma b/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/schema.prisma
new file mode 100644
index 000000000000..4363c97738ee
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/prisma/schema.prisma
@@ -0,0 +1,15 @@
+datasource db {
+ url = "postgresql://prisma:prisma@localhost:5433/tests"
+ provider = "postgresql"
+}
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+model User {
+ id Int @id @default(autoincrement())
+ createdAt DateTime @default(now())
+ email String @unique
+ name String?
+}
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts b/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts
new file mode 100644
index 000000000000..0eb40d9c83ee
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts
@@ -0,0 +1,47 @@
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+import { PrismaClient } from '@prisma/client';
+import * as Sentry from '@sentry/node';
+import { randomBytes } from 'crypto';
+
+const client = new PrismaClient();
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ integrations: [new Sentry.Integrations.Prisma({ client })],
+});
+
+async function run(): Promise {
+ const transaction = Sentry.startTransaction({
+ name: 'Test Transaction',
+ op: 'transaction',
+ });
+
+ Sentry.configureScope(scope => {
+ scope.setSpan(transaction);
+ });
+
+ try {
+ await client.user.create({
+ data: {
+ name: 'Tilda',
+ email: `tilda_${randomBytes(4).toString('hex')}@sentry.io`,
+ },
+ });
+
+ await client.user.findMany();
+
+ await client.user.deleteMany({
+ where: {
+ email: {
+ contains: 'sentry.io',
+ },
+ },
+ });
+ } finally {
+ if (transaction) transaction.finish();
+ }
+}
+
+void run();
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/setup.ts b/packages/node-integration-tests/suites/tracing-new/prisma-orm/setup.ts
new file mode 100755
index 000000000000..3c40d12f7337
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/setup.ts
@@ -0,0 +1,16 @@
+import { parseSemver } from '@sentry/utils';
+import { execSync } from 'child_process';
+
+const NODE_VERSION = parseSemver(process.versions.node);
+
+if (NODE_VERSION.major && NODE_VERSION.major < 12) {
+ // eslint-disable-next-line no-console
+ console.warn(`Skipping Prisma tests on Node: ${NODE_VERSION.major}`);
+ process.exit(0);
+}
+
+try {
+ execSync('yarn && yarn setup');
+} catch (_) {
+ process.exit(1);
+}
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/test.ts b/packages/node-integration-tests/suites/tracing-new/prisma-orm/test.ts
new file mode 100644
index 000000000000..e3393f5fe2f8
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/test.ts
@@ -0,0 +1,17 @@
+import { assertSentryTransaction, conditionalTest, TestEnv } from '../../../utils';
+
+conditionalTest({ min: 12 })('Prisma ORM Integration', () => {
+ test('should instrument Prisma client for tracing.', async () => {
+ const env = await TestEnv.init(__dirname);
+ const envelope = await env.getEnvelopeRequest({ envelopeType: 'transaction' });
+
+ assertSentryTransaction(envelope[2], {
+ transaction: 'Test Transaction',
+ spans: [
+ { description: 'User create', op: 'db.sql.prisma' },
+ { description: 'User findMany', op: 'db.sql.prisma' },
+ { description: 'User deleteMany', op: 'db.sql.prisma' },
+ ],
+ });
+ });
+});
diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/yarn.lock b/packages/node-integration-tests/suites/tracing-new/prisma-orm/yarn.lock
new file mode 100644
index 000000000000..d228adebd621
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/yarn.lock
@@ -0,0 +1,27 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@prisma/client@3.12.0":
+ version "3.12.0"
+ resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.12.0.tgz#a0eb49ffea5c128dd11dffb896d7139a60073d12"
+ integrity sha512-4NEQjUcWja/NVBvfuDFscWSk1/rXg3+wj+TSkqXCb1tKlx/bsUE00rxsvOvGg7VZ6lw1JFpGkwjwmsOIc4zvQw==
+ dependencies:
+ "@prisma/engines-version" "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980"
+
+"@prisma/engines-version@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980":
+ version "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980"
+ resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz#829ca3d9d0d92555f44644606d4edfd45b2f5886"
+ integrity sha512-o+jo8d7ZEiVpcpNWUDh3fj2uPQpBxl79XE9ih9nkogJbhw6P33274SHnqheedZ7PyvPIK/mvU8MLNYgetgXPYw==
+
+"@prisma/engines@3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980":
+ version "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980"
+ resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz#e52e364084c4d05278f62768047b788665e64a45"
+ integrity sha512-zULjkN8yhzS7B3yeEz4aIym4E2w1ChrV12i14pht3ePFufvsAvBSoZ+tuXMvfSoNTgBS5E4bolRzLbMmbwkkMQ==
+
+prisma@^3.12.0:
+ version "3.12.0"
+ resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.12.0.tgz#9675e0e72407122759d3eadcb6d27cdccd3497bd"
+ integrity sha512-ltCMZAx1i0i9xuPM692Srj8McC665h6E5RqJom999sjtVSccHSD8Z+HSdBN2183h9PJKvC5dapkn78dd0NWMBg==
+ dependencies:
+ "@prisma/engines" "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980"
diff --git a/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts b/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts
new file mode 100644
index 000000000000..a6197e5ab743
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts
@@ -0,0 +1,26 @@
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import * as Sentry from '@sentry/node';
+import * as http from 'http';
+
+Sentry.addTracingExtensions();
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ tracePropagationTargets: [/\/v0/, 'v1'],
+ integrations: [new Sentry.Integrations.Http({ tracing: true })],
+});
+
+const transaction = Sentry.startTransaction({ name: 'test_transaction' });
+
+Sentry.configureScope(scope => {
+ scope.setSpan(transaction);
+});
+
+http.get('http://match-this-url.com/api/v0');
+http.get('http://match-this-url.com/api/v1');
+http.get('http://dont-match-this-url.com/api/v2');
+http.get('http://dont-match-this-url.com/api/v3');
+
+transaction.finish();
diff --git a/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/test.ts b/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/test.ts
new file mode 100644
index 000000000000..1209c59da46a
--- /dev/null
+++ b/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/test.ts
@@ -0,0 +1,42 @@
+import nock from 'nock';
+
+import { runScenario, TestEnv } from '../../../utils';
+
+test('HttpIntegration should instrument correct requests when tracePropagationTargets option is provided', async () => {
+ const match1 = nock('http://match-this-url.com')
+ .get('/api/v0')
+ .matchHeader('baggage', val => typeof val === 'string')
+ .matchHeader('sentry-trace', val => typeof val === 'string')
+ .reply(200);
+
+ const match2 = nock('http://match-this-url.com')
+ .get('/api/v1')
+ .matchHeader('baggage', val => typeof val === 'string')
+ .matchHeader('sentry-trace', val => typeof val === 'string')
+ .reply(200);
+
+ const match3 = nock('http://dont-match-this-url.com')
+ .get('/api/v2')
+ .matchHeader('baggage', val => val === undefined)
+ .matchHeader('sentry-trace', val => val === undefined)
+ .reply(200);
+
+ const match4 = nock('http://dont-match-this-url.com')
+ .get('/api/v3')
+ .matchHeader('baggage', val => val === undefined)
+ .matchHeader('sentry-trace', val => val === undefined)
+ .reply(200);
+
+ const env = await TestEnv.init(__dirname);
+ await runScenario(env.url);
+
+ env.server.close();
+ nock.cleanAll();
+
+ await new Promise(resolve => env.server.close(resolve));
+
+ expect(match1.isDone()).toBe(true);
+ expect(match2.isDone()).toBe(true);
+ expect(match3.isDone()).toBe(true);
+ expect(match4.isDone()).toBe(true);
+});
diff --git a/packages/node/package.json b/packages/node/package.json
index 7386be3912bb..e29e7290c495 100644
--- a/packages/node/package.json
+++ b/packages/node/package.json
@@ -19,6 +19,7 @@
"@sentry/core": "7.44.2",
"@sentry/types": "7.44.2",
"@sentry/utils": "7.44.2",
+ "@sentry-internal/tracing": "7.44.2",
"cookie": "^0.4.1",
"https-proxy-agent": "^5.0.0",
"lru_map": "^0.3.3",
diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts
index 0b3a925d775e..d0d0ae7424be 100644
--- a/packages/node/src/client.ts
+++ b/packages/node/src/client.ts
@@ -1,5 +1,5 @@
import type { Scope } from '@sentry/core';
-import { BaseClient, SDK_VERSION, SessionFlusher } from '@sentry/core';
+import { addTracingExtensions, BaseClient, SDK_VERSION, SessionFlusher } from '@sentry/core';
import type { Event, EventHint, Severity, SeverityLevel } from '@sentry/types';
import { logger, resolvedSyncPromise } from '@sentry/utils';
import * as os from 'os';
@@ -40,6 +40,9 @@ export class NodeClient extends BaseClient {
...options.transportOptions,
};
+ // The Node client always supports tracing
+ addTracingExtensions();
+
super(options);
}
diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts
index 92d3cfcf835f..07db6be2f07d 100644
--- a/packages/node/src/index.ts
+++ b/packages/node/src/index.ts
@@ -45,6 +45,7 @@ export {
setUser,
withScope,
} from '@sentry/core';
+export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing';
export { NodeClient } from './client';
export { makeNodeTransport } from './transports';
@@ -57,10 +58,12 @@ import * as domain from 'domain';
import * as Handlers from './handlers';
import * as NodeIntegrations from './integrations';
+import * as TracingIntegrations from './tracing/integrations';
const INTEGRATIONS = {
...CoreIntegrations,
...NodeIntegrations,
+ ...TracingIntegrations,
};
export { INTEGRATIONS as Integrations, Handlers };
diff --git a/packages/node/src/tracing/index.ts b/packages/node/src/tracing/index.ts
new file mode 100644
index 000000000000..15c4e2889b3f
--- /dev/null
+++ b/packages/node/src/tracing/index.ts
@@ -0,0 +1,24 @@
+import { lazyLoadedNodePerformanceMonitoringIntegrations } from '@sentry-internal/tracing';
+import type { Integration } from '@sentry/types';
+import { logger } from '@sentry/utils';
+
+/**
+ * Automatically detects and returns integrations that will work with your dependencies.
+ */
+export function autoDiscoverNodePerformanceMonitoringIntegrations(): Integration[] {
+ const loadedIntegrations = lazyLoadedNodePerformanceMonitoringIntegrations
+ .map(tryLoad => {
+ try {
+ return tryLoad();
+ } catch (_) {
+ return undefined;
+ }
+ })
+ .filter(integration => !!integration) as Integration[];
+
+ if (loadedIntegrations.length === 0) {
+ logger.warn('Performance monitoring integrations could not be automatically loaded.');
+ }
+
+ return loadedIntegrations;
+}
diff --git a/packages/node/src/tracing/integrations.ts b/packages/node/src/tracing/integrations.ts
new file mode 100644
index 000000000000..a37bf6bfd494
--- /dev/null
+++ b/packages/node/src/tracing/integrations.ts
@@ -0,0 +1 @@
+export { Apollo, Express, GraphQL, Mongo, Mysql, Postgres, Prisma } from '@sentry-internal/tracing';
diff --git a/packages/tracing-internal/src/index.ts b/packages/tracing-internal/src/index.ts
index c735942fbe19..c4e17c25294f 100644
--- a/packages/tracing-internal/src/index.ts
+++ b/packages/tracing-internal/src/index.ts
@@ -1,6 +1,15 @@
export * from './exports';
-export { Apollo, Express, GraphQL, Mongo, Mysql, Postgres, Prisma } from './node/integrations';
+export {
+ Apollo,
+ Express,
+ GraphQL,
+ Mongo,
+ Mysql,
+ Postgres,
+ Prisma,
+ lazyLoadedNodePerformanceMonitoringIntegrations,
+} from './node';
export {
BrowserTracing,
diff --git a/packages/tracing-internal/src/node/integrations/index.ts b/packages/tracing-internal/src/node/integrations/index.ts
index 607a3e129984..0b69f4440f3a 100644
--- a/packages/tracing-internal/src/node/integrations/index.ts
+++ b/packages/tracing-internal/src/node/integrations/index.ts
@@ -5,3 +5,4 @@ export { Mongo } from './mongo';
export { Prisma } from './prisma';
export { GraphQL } from './graphql';
export { Apollo } from './apollo';
+export * from './lazy';
diff --git a/packages/tracing-internal/src/node/integrations/lazy.ts b/packages/tracing-internal/src/node/integrations/lazy.ts
new file mode 100644
index 000000000000..f53ff756cd48
--- /dev/null
+++ b/packages/tracing-internal/src/node/integrations/lazy.ts
@@ -0,0 +1,47 @@
+import type { Integration, IntegrationClass } from '@sentry/types';
+import { dynamicRequire } from '@sentry/utils';
+
+export const lazyLoadedNodePerformanceMonitoringIntegrations: (() => Integration)[] = [
+ () => {
+ const integration = dynamicRequire(module, './apollo') as {
+ Apollo: IntegrationClass;
+ };
+ return new integration.Apollo();
+ },
+ () => {
+ const integration = dynamicRequire(module, './apollo') as {
+ Apollo: IntegrationClass;
+ };
+ return new integration.Apollo({ useNestjs: true });
+ },
+ () => {
+ const integration = dynamicRequire(module, './graphql') as {
+ GraphQL: IntegrationClass;
+ };
+ return new integration.GraphQL();
+ },
+ () => {
+ const integration = dynamicRequire(module, './mongo') as {
+ Mongo: IntegrationClass;
+ };
+ return new integration.Mongo();
+ },
+ () => {
+ const integration = dynamicRequire(module, './mongo') as {
+ Mongo: IntegrationClass;
+ };
+ return new integration.Mongo({ mongoose: true });
+ },
+ () => {
+ const integration = dynamicRequire(module, './mysql') as {
+ Mysql: IntegrationClass;
+ };
+ return new integration.Mysql();
+ },
+ () => {
+ const integration = dynamicRequire(module, './postgres') as {
+ Postgres: IntegrationClass;
+ };
+ return new integration.Postgres();
+ },
+];
From 86b89b9a271ba4bfe66dd74284992a07e1bc9c72 Mon Sep 17 00:00:00 2001
From: Tim Fish
Date: Tue, 21 Mar 2023 19:11:05 +0000
Subject: [PATCH 08/34] feat(tracing): Migrate some imports away from
`@sentry/tracing` (#7539)
---
packages/nextjs/src/server/wrapApiHandlerWithSentry.ts | 2 +-
packages/opentelemetry-node/src/spanprocessor.ts | 3 +--
.../opentelemetry-node/src/utils/map-otel-status.ts | 2 +-
packages/remix/src/utils/instrumentServer.ts | 3 +--
packages/serverless/src/awslambda.ts | 10 ++++++++--
packages/serverless/src/gcpfunction/http.ts | 2 +-
6 files changed, 13 insertions(+), 9 deletions(-)
diff --git a/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts
index 5bed5dca3135..37ef93bf30cc 100644
--- a/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts
+++ b/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts
@@ -1,10 +1,10 @@
import { hasTracingEnabled } from '@sentry/core';
import { captureException, getCurrentHub, startTransaction } from '@sentry/node';
-import { extractTraceparentData } from '@sentry/tracing';
import type { Transaction } from '@sentry/types';
import {
addExceptionMechanism,
baggageHeaderToDynamicSamplingContext,
+ extractTraceparentData,
isString,
logger,
objectify,
diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts
index 08d14e9fa671..c2d141e05f56 100644
--- a/packages/opentelemetry-node/src/spanprocessor.ts
+++ b/packages/opentelemetry-node/src/spanprocessor.ts
@@ -2,8 +2,7 @@ import type { Context } from '@opentelemetry/api';
import { SpanKind, trace } from '@opentelemetry/api';
import type { Span as OtelSpan, SpanProcessor as OtelSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
-import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core';
-import { Transaction } from '@sentry/tracing';
+import { addGlobalEventProcessor, getCurrentHub, Transaction } from '@sentry/core';
import type { DynamicSamplingContext, Span as SentrySpan, TraceparentData, TransactionContext } from '@sentry/types';
import { isString, logger } from '@sentry/utils';
diff --git a/packages/opentelemetry-node/src/utils/map-otel-status.ts b/packages/opentelemetry-node/src/utils/map-otel-status.ts
index 968150852e6e..8fdc0e09ac8b 100644
--- a/packages/opentelemetry-node/src/utils/map-otel-status.ts
+++ b/packages/opentelemetry-node/src/utils/map-otel-status.ts
@@ -1,6 +1,6 @@
import type { Span as OtelSpan } from '@opentelemetry/sdk-trace-base';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
-import type { SpanStatusType as SentryStatus } from '@sentry/tracing';
+import type { SpanStatusType as SentryStatus } from '@sentry/core';
// canonicalCodesHTTPMap maps some HTTP codes to Sentry's span statuses. See possible mapping in https://develop.sentry.dev/sdk/event-payloads/span/
const canonicalCodesHTTPMap: Record = {
diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts
index 43e4d8cd1bf0..825198426eec 100644
--- a/packages/remix/src/utils/instrumentServer.ts
+++ b/packages/remix/src/utils/instrumentServer.ts
@@ -1,8 +1,7 @@
/* eslint-disable max-lines */
-import { hasTracingEnabled } from '@sentry/core';
+import { getActiveTransaction, hasTracingEnabled } from '@sentry/core';
import type { Hub } from '@sentry/node';
import { captureException, getCurrentHub } from '@sentry/node';
-import { getActiveTransaction } from '@sentry/tracing';
import type { Transaction, TransactionSource, WrappedFunction } from '@sentry/types';
import {
addExceptionMechanism,
diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts
index 29dd10da6602..ccfc1e191024 100644
--- a/packages/serverless/src/awslambda.ts
+++ b/packages/serverless/src/awslambda.ts
@@ -2,9 +2,15 @@
import type { Scope } from '@sentry/node';
import * as Sentry from '@sentry/node';
import { captureException, captureMessage, flush, getCurrentHub, withScope } from '@sentry/node';
-import { extractTraceparentData } from '@sentry/tracing';
import type { Integration } from '@sentry/types';
-import { baggageHeaderToDynamicSamplingContext, dsnFromString, dsnToString, isString, logger } from '@sentry/utils';
+import {
+ baggageHeaderToDynamicSamplingContext,
+ dsnFromString,
+ dsnToString,
+ extractTraceparentData,
+ isString,
+ logger,
+} from '@sentry/utils';
// NOTE: I have no idea how to fix this right now, and don't want to waste more time, as it builds just fine — Kamil
// eslint-disable-next-line import/no-unresolved
import type { Context, Handler } from 'aws-lambda';
diff --git a/packages/serverless/src/gcpfunction/http.ts b/packages/serverless/src/gcpfunction/http.ts
index fbe5882f549a..8892353fd4bf 100644
--- a/packages/serverless/src/gcpfunction/http.ts
+++ b/packages/serverless/src/gcpfunction/http.ts
@@ -1,8 +1,8 @@
import type { AddRequestDataToEventOptions } from '@sentry/node';
import { captureException, flush, getCurrentHub } from '@sentry/node';
-import { extractTraceparentData } from '@sentry/tracing';
import {
baggageHeaderToDynamicSamplingContext,
+ extractTraceparentData,
isString,
isThenable,
logger,
From baff7dd73f54a070250fd28e88fa30d99a41ce50 Mon Sep 17 00:00:00 2001
From: Tim Fish
Date: Wed, 22 Mar 2023 10:05:18 +0000
Subject: [PATCH 09/34] feat(nextjs): Remove `@sentry/tracing` dependency from
nextjs SDK (#7561)
---
packages/browser/src/index.ts | 4 ++--
packages/nextjs/package.json | 1 -
packages/nextjs/src/client/index.ts | 9 +++++++--
packages/nextjs/src/edge/edgeclient.ts | 5 ++++-
packages/nextjs/src/edge/index.ts | 2 --
packages/nextjs/src/server/utils/wrapperUtils.ts | 3 +--
packages/nextjs/test/config/wrappers.test.ts | 5 +++++
packages/nextjs/test/edge/withSentryAPI.test.ts | 4 ++++
packages/node/src/index.ts | 1 +
9 files changed, 24 insertions(+), 10 deletions(-)
diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts
index 59d814ac9412..dca32f05b35d 100644
--- a/packages/browser/src/index.ts
+++ b/packages/browser/src/index.ts
@@ -21,8 +21,8 @@ const INTEGRATIONS = {
export { INTEGRATIONS as Integrations };
export { Replay } from '@sentry/replay';
-export { BrowserTracing } from '@sentry-internal/tracing';
-export { addTracingExtensions } from '@sentry/core';
+export { BrowserTracing, defaultRequestInstrumentationOptions } from '@sentry-internal/tracing';
+export { addTracingExtensions, getActiveTransaction } from '@sentry/core';
export { makeBrowserOfflineTransport } from './transports/offline';
export { onProfilingStartRouteTransaction } from './profiling/hubextensions';
export { BrowserProfilingIntegration } from './profiling/integration';
diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json
index f341b0609519..8b32553b1f9d 100644
--- a/packages/nextjs/package.json
+++ b/packages/nextjs/package.json
@@ -22,7 +22,6 @@
"@sentry/integrations": "7.44.2",
"@sentry/node": "7.44.2",
"@sentry/react": "7.44.2",
- "@sentry/tracing": "7.44.2",
"@sentry/types": "7.44.2",
"@sentry/utils": "7.44.2",
"@sentry/webpack-plugin": "1.20.0",
diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts
index 79dcd5219cd4..ece6bf78db6a 100644
--- a/packages/nextjs/src/client/index.ts
+++ b/packages/nextjs/src/client/index.ts
@@ -1,8 +1,13 @@
import { hasTracingEnabled } from '@sentry/core';
import { RewriteFrames } from '@sentry/integrations';
import type { BrowserOptions } from '@sentry/react';
-import { configureScope, init as reactInit, Integrations } from '@sentry/react';
-import { BrowserTracing, defaultRequestInstrumentationOptions } from '@sentry/tracing';
+import {
+ BrowserTracing,
+ configureScope,
+ defaultRequestInstrumentationOptions,
+ init as reactInit,
+ Integrations,
+} from '@sentry/react';
import type { EventProcessor } from '@sentry/types';
import { addOrUpdateIntegration } from '@sentry/utils';
diff --git a/packages/nextjs/src/edge/edgeclient.ts b/packages/nextjs/src/edge/edgeclient.ts
index a5b38d651aed..16aed66d1ca4 100644
--- a/packages/nextjs/src/edge/edgeclient.ts
+++ b/packages/nextjs/src/edge/edgeclient.ts
@@ -1,5 +1,5 @@
import type { Scope } from '@sentry/core';
-import { BaseClient, SDK_VERSION } from '@sentry/core';
+import { addTracingExtensions, BaseClient, SDK_VERSION } from '@sentry/core';
import type { ClientOptions, Event, EventHint, Severity, SeverityLevel } from '@sentry/types';
import { eventFromMessage, eventFromUnknownInput } from './eventbuilder';
@@ -28,6 +28,9 @@ export class EdgeClient extends BaseClient {
version: SDK_VERSION,
};
+ // The Edge client always supports tracing
+ addTracingExtensions();
+
super(options);
}
diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts
index 6f8cd2f42cc4..f7785aaa06d6 100644
--- a/packages/nextjs/src/edge/index.ts
+++ b/packages/nextjs/src/edge/index.ts
@@ -1,5 +1,3 @@
-import '@sentry/tracing'; // Allow people to call tracing API methods without explicitly importing the tracing package.
-
import { getCurrentHub, getIntegrationsToSetup, initAndBind, Integrations as CoreIntegrations } from '@sentry/core';
import type { Options } from '@sentry/types';
import {
diff --git a/packages/nextjs/src/server/utils/wrapperUtils.ts b/packages/nextjs/src/server/utils/wrapperUtils.ts
index ae05b3f16b8d..9fa91fbbee5a 100644
--- a/packages/nextjs/src/server/utils/wrapperUtils.ts
+++ b/packages/nextjs/src/server/utils/wrapperUtils.ts
@@ -1,5 +1,4 @@
-import { captureException, getCurrentHub, startTransaction } from '@sentry/core';
-import { getActiveTransaction } from '@sentry/tracing';
+import { captureException, getActiveTransaction, getCurrentHub, startTransaction } from '@sentry/core';
import type { Transaction } from '@sentry/types';
import { baggageHeaderToDynamicSamplingContext, extractTraceparentData } from '@sentry/utils';
import * as domain from 'domain';
diff --git a/packages/nextjs/test/config/wrappers.test.ts b/packages/nextjs/test/config/wrappers.test.ts
index 68c598e9707f..444d45513ccb 100644
--- a/packages/nextjs/test/config/wrappers.test.ts
+++ b/packages/nextjs/test/config/wrappers.test.ts
@@ -1,4 +1,5 @@
import * as SentryCore from '@sentry/core';
+import { addTracingExtensions } from '@sentry/core';
import * as SentryNode from '@sentry/node';
import type { IncomingMessage, ServerResponse } from 'http';
@@ -6,6 +7,10 @@ import { wrapGetInitialPropsWithSentry, wrapGetServerSidePropsWithSentry } from
const startTransactionSpy = jest.spyOn(SentryCore, 'startTransaction');
+// The wrap* functions require the hub to have tracing extensions. This is normally called by the NodeClient
+// constructor but the client isn't used in these tests.
+addTracingExtensions();
+
describe('data-fetching function wrappers', () => {
const route = '/tricks/[trickName]';
let req: IncomingMessage;
diff --git a/packages/nextjs/test/edge/withSentryAPI.test.ts b/packages/nextjs/test/edge/withSentryAPI.test.ts
index 2ecbdf22a96e..08a91e0c5e11 100644
--- a/packages/nextjs/test/edge/withSentryAPI.test.ts
+++ b/packages/nextjs/test/edge/withSentryAPI.test.ts
@@ -2,6 +2,10 @@ import * as coreSdk from '@sentry/core';
import { wrapApiHandlerWithSentry } from '../../src/edge';
+// The wrap* functions require the hub to have tracing extensions. This is normally called by the EdgeClient
+// constructor but the client isn't used in these tests.
+coreSdk.addTracingExtensions();
+
// @ts-ignore Request does not exist on type Global
const origRequest = global.Request;
// @ts-ignore Response does not exist on type Global
diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts
index 07db6be2f07d..7f0d923e4ae2 100644
--- a/packages/node/src/index.ts
+++ b/packages/node/src/index.ts
@@ -30,6 +30,7 @@ export {
captureMessage,
configureScope,
createTransport,
+ getActiveTransaction,
getHubFromCarrier,
getCurrentHub,
Hub,
From a73f58bb79c337cfbbcd66ea5565400892a2d144 Mon Sep 17 00:00:00 2001
From: Tim Fish
Date: Wed, 22 Mar 2023 10:35:43 +0000
Subject: [PATCH 10/34] fix(node): Consider tracing error handler for process
exit (#7558)
---
packages/core/src/tracing/errors.ts | 4 +++
.../src/integrations/onuncaughtexception.ts | 31 ++++++++++++-------
2 files changed, 23 insertions(+), 12 deletions(-)
diff --git a/packages/core/src/tracing/errors.ts b/packages/core/src/tracing/errors.ts
index 3351f428fecf..8785bd86d448 100644
--- a/packages/core/src/tracing/errors.ts
+++ b/packages/core/src/tracing/errors.ts
@@ -29,3 +29,7 @@ function errorCallback(): void {
activeTransaction.setStatus(status);
}
}
+
+// The function name will be lost when bundling but we need to be able to identify this listener later to maintain the
+// node.js default exit behaviour
+errorCallback.tag = 'sentry_tracingErrorCallback';
diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts
index 2d10ae61d696..e55de4d1fd8e 100644
--- a/packages/node/src/integrations/onuncaughtexception.ts
+++ b/packages/node/src/integrations/onuncaughtexception.ts
@@ -8,6 +8,10 @@ import { logAndExitProcess } from './utils/errorhandling';
type OnFatalErrorHandler = (firstError: Error, secondError?: Error) => void;
+type TaggedListener = NodeJS.UncaughtExceptionListener & {
+ tag?: string;
+};
+
// CAREFUL: Please think twice before updating the way _options looks because the Next.js SDK depends on it in `index.server.ts`
interface OnUncaughtExceptionOptions {
// TODO(v8): Evaluate whether we should switch the default behaviour here.
@@ -95,18 +99,21 @@ export class OnUncaughtException implements Integration {
// exit behaviour of the SDK accordingly:
// - If other listeners are attached, do not exit.
// - If the only listener attached is ours, exit.
- const userProvidedListenersCount = global.process
- .listeners('uncaughtException')
- .reduce((acc, listener) => {
- if (
- listener.name === 'domainUncaughtExceptionClear' || // as soon as we're using domains this listener is attached by node itself
- listener === this.handler // filter the handler we registered ourselves)
- ) {
- return acc;
- } else {
- return acc + 1;
- }
- }, 0);
+ const userProvidedListenersCount = (
+ global.process.listeners('uncaughtException') as TaggedListener[]
+ ).reduce((acc, listener) => {
+ if (
+ // There are 3 listeners we ignore:
+ listener.name === 'domainUncaughtExceptionClear' || // as soon as we're using domains this listener is attached by node itself
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ (listener.tag && listener.tag === 'sentry_tracingErrorCallback') || // the handler we register for tracing
+ listener === this.handler // the handler we register in this integration
+ ) {
+ return acc;
+ } else {
+ return acc + 1;
+ }
+ }, 0);
const processWouldExit = userProvidedListenersCount === 0;
const shouldApplyFatalHandlingLogic = this._options.exitEvenIfOtherHandlersAreRegistered || processWouldExit;
From 4f34b5ae8900546a39ee112ebff4633dab12c3cd Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Wed, 22 Mar 2023 11:40:00 +0100
Subject: [PATCH 11/34] feat(core): Add trace function (#7556)
```js
const fetchResult = Sentry.trace({ name: 'GET /users'}, () => fetch('/users'), handleError);
```
---
packages/core/src/tracing/index.ts | 1 +
packages/core/src/tracing/trace.ts | 65 +++++++
packages/core/test/lib/tracing/trace.test.ts | 170 +++++++++++++++++++
3 files changed, 236 insertions(+)
create mode 100644 packages/core/src/tracing/trace.ts
create mode 100644 packages/core/test/lib/tracing/trace.test.ts
diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts
index fd4949257ceb..1afb556bce4d 100644
--- a/packages/core/src/tracing/index.ts
+++ b/packages/core/src/tracing/index.ts
@@ -6,3 +6,4 @@ export { extractTraceparentData, getActiveTransaction, stripUrlQueryAndFragment,
// eslint-disable-next-line deprecation/deprecation
export { SpanStatus } from './spanstatus';
export type { SpanStatusType } from './span';
+export { trace } from './trace';
diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts
new file mode 100644
index 000000000000..19911f7be91f
--- /dev/null
+++ b/packages/core/src/tracing/trace.ts
@@ -0,0 +1,65 @@
+import type { TransactionContext } from '@sentry/types';
+import { isThenable } from '@sentry/utils';
+
+import { getCurrentHub } from '../hub';
+import type { Span } from './span';
+
+/**
+ * Wraps a function with a transaction/span and finishes the span after the function is done.
+ *
+ * This function is meant to be used internally and may break at any time. Use at your own risk.
+ *
+ * @internal
+ * @private
+ */
+export function trace(
+ context: TransactionContext,
+ callback: (span: Span) => T,
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ onError: (error: unknown) => void = () => {},
+): T {
+ const ctx = { ...context };
+ // If a name is set and a description is not, set the description to the name.
+ if (ctx.name !== undefined && ctx.description === undefined) {
+ ctx.description = ctx.name;
+ }
+
+ const hub = getCurrentHub();
+ const scope = hub.getScope();
+
+ const parentSpan = scope.getSpan();
+ const activeSpan = parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx);
+ scope.setSpan(activeSpan);
+
+ function finishAndSetSpan(): void {
+ activeSpan.finish();
+ hub.getScope().setSpan(parentSpan);
+ }
+
+ let maybePromiseResult: T;
+ try {
+ maybePromiseResult = callback(activeSpan);
+ } catch (e) {
+ activeSpan.setStatus('internal_error');
+ onError(e);
+ finishAndSetSpan();
+ throw e;
+ }
+
+ if (isThenable(maybePromiseResult)) {
+ Promise.resolve(maybePromiseResult).then(
+ () => {
+ finishAndSetSpan();
+ },
+ e => {
+ activeSpan.setStatus('internal_error');
+ onError(e);
+ finishAndSetSpan();
+ },
+ );
+ } else {
+ finishAndSetSpan();
+ }
+
+ return maybePromiseResult;
+}
diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts
new file mode 100644
index 000000000000..8a7aa4e09191
--- /dev/null
+++ b/packages/core/test/lib/tracing/trace.test.ts
@@ -0,0 +1,170 @@
+import { addTracingExtensions, Hub, makeMain } from '../../../src';
+import { trace } from '../../../src/tracing';
+import { getDefaultTestClientOptions, TestClient } from '../../mocks/client';
+
+beforeAll(() => {
+ addTracingExtensions();
+});
+
+const enum Type {
+ Sync = 'sync',
+ Async = 'async',
+}
+
+let hub: Hub;
+let client: TestClient;
+
+describe('trace', () => {
+ beforeEach(() => {
+ const options = getDefaultTestClientOptions({ tracesSampleRate: 0.0 });
+ client = new TestClient(options);
+ hub = new Hub(client);
+ makeMain(hub);
+ });
+
+ describe.each([
+ // isSync, isError, callback, expectedReturnValue
+ [Type.Async, false, () => Promise.resolve('async good'), 'async good'],
+ [Type.Sync, false, () => 'sync good', 'sync good'],
+ [Type.Async, true, () => Promise.reject('async bad'), 'async bad'],
+ [
+ Type.Sync,
+ true,
+ () => {
+ throw 'sync bad';
+ },
+ 'sync bad',
+ ],
+ ])('with %s callback and error %s', (_type, isError, callback, expected) => {
+ it('should return the same value as the callback', async () => {
+ try {
+ const result = await trace({ name: 'GET users/[id]' }, () => {
+ return callback();
+ });
+ expect(result).toEqual(expected);
+ } catch (e) {
+ expect(e).toEqual(expected);
+ }
+ });
+
+ it('creates a transaction', async () => {
+ let ref: any = undefined;
+ client.on('finishTransaction', transaction => {
+ ref = transaction;
+ });
+ try {
+ await trace({ name: 'GET users/[id]' }, () => {
+ return callback();
+ });
+ } catch (e) {
+ //
+ }
+ expect(ref).toBeDefined();
+
+ expect(ref.name).toEqual('GET users/[id]');
+ expect(ref.status).toEqual(isError ? 'internal_error' : undefined);
+ });
+
+ it('allows traceparent information to be overriden', async () => {
+ let ref: any = undefined;
+ client.on('finishTransaction', transaction => {
+ ref = transaction;
+ });
+ try {
+ await trace(
+ {
+ name: 'GET users/[id]',
+ parentSampled: true,
+ traceId: '12345678901234567890123456789012',
+ parentSpanId: '1234567890123456',
+ },
+ () => {
+ return callback();
+ },
+ );
+ } catch (e) {
+ //
+ }
+ expect(ref).toBeDefined();
+
+ expect(ref.sampled).toEqual(true);
+ expect(ref.traceId).toEqual('12345678901234567890123456789012');
+ expect(ref.parentSpanId).toEqual('1234567890123456');
+ });
+
+ it('allows for transaction to be mutated', async () => {
+ let ref: any = undefined;
+ client.on('finishTransaction', transaction => {
+ ref = transaction;
+ });
+ try {
+ await trace({ name: 'GET users/[id]' }, span => {
+ span.op = 'http.server';
+ return callback();
+ });
+ } catch (e) {
+ //
+ }
+
+ expect(ref.op).toEqual('http.server');
+ });
+
+ it('creates a span with correct description', async () => {
+ let ref: any = undefined;
+ client.on('finishTransaction', transaction => {
+ ref = transaction;
+ });
+ try {
+ await trace({ name: 'GET users/[id]', parentSampled: true }, () => {
+ return trace({ name: 'SELECT * from users' }, () => {
+ return callback();
+ });
+ });
+ } catch (e) {
+ //
+ }
+
+ expect(ref.spanRecorder.spans).toHaveLength(2);
+ expect(ref.spanRecorder.spans[1].description).toEqual('SELECT * from users');
+ expect(ref.spanRecorder.spans[1].parentSpanId).toEqual(ref.spanId);
+ expect(ref.spanRecorder.spans[1].status).toEqual(isError ? 'internal_error' : undefined);
+ });
+
+ it('allows for span to be mutated', async () => {
+ let ref: any = undefined;
+ client.on('finishTransaction', transaction => {
+ ref = transaction;
+ });
+ try {
+ await trace({ name: 'GET users/[id]', parentSampled: true }, () => {
+ return trace({ name: 'SELECT * from users' }, childSpan => {
+ childSpan.op = 'db.query';
+ return callback();
+ });
+ });
+ } catch (e) {
+ //
+ }
+
+ expect(ref.spanRecorder.spans).toHaveLength(2);
+ expect(ref.spanRecorder.spans[1].op).toEqual('db.query');
+ });
+
+ it('calls `onError` hook', async () => {
+ const onError = jest.fn();
+ try {
+ await trace(
+ { name: 'GET users/[id]' },
+ () => {
+ return callback();
+ },
+ onError,
+ );
+ } catch (e) {
+ expect(onError).toHaveBeenCalledTimes(1);
+ expect(onError).toHaveBeenCalledWith(e);
+ }
+ expect(onError).toHaveBeenCalledTimes(isError ? 1 : 0);
+ });
+ });
+});
From 89b5720ad8f416848b239370a6a7956ee8c040a4 Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Wed, 22 Mar 2023 14:38:03 +0100
Subject: [PATCH 12/34] fix(tracing): Account for case where startTransaction
returns undefined (#7566)
---
packages/core/src/tracing/trace.ts | 11 ++++++----
packages/core/test/lib/tracing/trace.test.ts | 23 ++++++++++++++++++--
2 files changed, 28 insertions(+), 6 deletions(-)
diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts
index 19911f7be91f..8e7844d23988 100644
--- a/packages/core/src/tracing/trace.ts
+++ b/packages/core/src/tracing/trace.ts
@@ -7,6 +7,9 @@ import type { Span } from './span';
/**
* Wraps a function with a transaction/span and finishes the span after the function is done.
*
+ * Note that if you have not enabled tracing extensions via `addTracingExtensions`, this function
+ * will not generate spans, and the `span` returned from the callback may be undefined.
+ *
* This function is meant to be used internally and may break at any time. Use at your own risk.
*
* @internal
@@ -14,7 +17,7 @@ import type { Span } from './span';
*/
export function trace(
context: TransactionContext,
- callback: (span: Span) => T,
+ callback: (span?: Span) => T,
// eslint-disable-next-line @typescript-eslint/no-empty-function
onError: (error: unknown) => void = () => {},
): T {
@@ -32,7 +35,7 @@ export function trace(
scope.setSpan(activeSpan);
function finishAndSetSpan(): void {
- activeSpan.finish();
+ activeSpan && activeSpan.finish();
hub.getScope().setSpan(parentSpan);
}
@@ -40,7 +43,7 @@ export function trace(
try {
maybePromiseResult = callback(activeSpan);
} catch (e) {
- activeSpan.setStatus('internal_error');
+ activeSpan && activeSpan.setStatus('internal_error');
onError(e);
finishAndSetSpan();
throw e;
@@ -52,7 +55,7 @@ export function trace(
finishAndSetSpan();
},
e => {
- activeSpan.setStatus('internal_error');
+ activeSpan && activeSpan.setStatus('internal_error');
onError(e);
finishAndSetSpan();
},
diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts
index 8a7aa4e09191..064c41dc123a 100644
--- a/packages/core/test/lib/tracing/trace.test.ts
+++ b/packages/core/test/lib/tracing/trace.test.ts
@@ -47,6 +47,21 @@ describe('trace', () => {
}
});
+ it('should return the same value as the callback if transactions are undefined', async () => {
+ // @ts-ignore we are force overriding the transaction return to be undefined
+ // The `startTransaction` types are actually wrong - it can return undefined
+ // if tracingExtensions are not enabled
+ jest.spyOn(hub, 'startTransaction').mockReturnValue(undefined);
+ try {
+ const result = await trace({ name: 'GET users/[id]' }, () => {
+ return callback();
+ });
+ expect(result).toEqual(expected);
+ } catch (e) {
+ expect(e).toEqual(expected);
+ }
+ });
+
it('creates a transaction', async () => {
let ref: any = undefined;
client.on('finishTransaction', transaction => {
@@ -99,7 +114,9 @@ describe('trace', () => {
});
try {
await trace({ name: 'GET users/[id]' }, span => {
- span.op = 'http.server';
+ if (span) {
+ span.op = 'http.server';
+ }
return callback();
});
} catch (e) {
@@ -138,7 +155,9 @@ describe('trace', () => {
try {
await trace({ name: 'GET users/[id]', parentSampled: true }, () => {
return trace({ name: 'SELECT * from users' }, childSpan => {
- childSpan.op = 'db.query';
+ if (childSpan) {
+ childSpan.op = 'db.query';
+ }
return callback();
});
});
From e97b0970f9eefe927562efed19383b08144aede8 Mon Sep 17 00:00:00 2001
From: Lukas Stracke
Date: Wed, 22 Mar 2023 15:12:40 +0100
Subject: [PATCH 13/34] feat(sveltekit): Add SvelteKit routing instrumentation
(#7565)
Add routing instrumentation to the client SvelteKit SDK. Pageload navigations are created on function call as always and updated with a proper name once the `page` store emits the route id. Navigation transactions are created by subscribing to the `navigating` store. No need for user configuration, as the instrumentation will be added automatically on SDK intialization.
---
packages/sveltekit/package.json | 1 +
packages/sveltekit/rollup.npm.config.js | 20 +--
packages/sveltekit/src/client/router.ts | 111 ++++++++++++++
packages/sveltekit/src/client/sdk.ts | 12 +-
packages/sveltekit/test/client/router.test.ts | 139 ++++++++++++++++++
packages/sveltekit/test/client/sdk.test.ts | 25 +++-
packages/sveltekit/test/vitest.setup.ts | 13 ++
packages/sveltekit/vite.config.ts | 13 +-
yarn.lock | 5 +
9 files changed, 313 insertions(+), 26 deletions(-)
create mode 100644 packages/sveltekit/src/client/router.ts
create mode 100644 packages/sveltekit/test/client/router.test.ts
create mode 100644 packages/sveltekit/test/vitest.setup.ts
diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json
index ab2e9ee19ac7..2293cdcd42cb 100644
--- a/packages/sveltekit/package.json
+++ b/packages/sveltekit/package.json
@@ -30,6 +30,7 @@
},
"devDependencies": {
"@sveltejs/kit": "^1.11.0",
+ "svelte": "^3.44.0",
"typescript": "^4.9.3",
"vite": "4.0.0"
},
diff --git a/packages/sveltekit/rollup.npm.config.js b/packages/sveltekit/rollup.npm.config.js
index f1f8240d5a7a..f9dfe71fd30c 100644
--- a/packages/sveltekit/rollup.npm.config.js
+++ b/packages/sveltekit/rollup.npm.config.js
@@ -1,14 +1,10 @@
import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js';
-export default
- makeNPMConfigVariants(
- makeBaseNPMConfig({
- entrypoints: [
- 'src/index.server.ts',
- 'src/index.client.ts',
- 'src/client/index.ts',
- 'src/server/index.ts',
- ],
- }),
- )
-;
+export default makeNPMConfigVariants(
+ makeBaseNPMConfig({
+ entrypoints: ['src/index.server.ts', 'src/index.client.ts', 'src/client/index.ts', 'src/server/index.ts'],
+ packageSpecificConfig: {
+ external: ['$app/stores'],
+ },
+ }),
+);
diff --git a/packages/sveltekit/src/client/router.ts b/packages/sveltekit/src/client/router.ts
new file mode 100644
index 000000000000..c4cb7a95c5cf
--- /dev/null
+++ b/packages/sveltekit/src/client/router.ts
@@ -0,0 +1,111 @@
+import { getActiveTransaction } from '@sentry/core';
+import { WINDOW } from '@sentry/svelte';
+import type { Span, Transaction, TransactionContext } from '@sentry/types';
+
+import { navigating, page } from '$app/stores';
+
+const DEFAULT_TAGS = {
+ 'routing.instrumentation': '@sentry/sveltekit',
+};
+
+/**
+ * Automatically creates pageload and navigation transactions for the client-side SvelteKit router.
+ *
+ * This instrumentation makes use of SvelteKit's `page` and `navigating` stores which can be accessed
+ * anywhere on the client side.
+ *
+ * @param startTransactionFn the function used to start (idle) transactions
+ * @param startTransactionOnPageLoad controls if pageload transactions should be created (defaults to `true`)
+ * @param startTransactionOnLocationChange controls if navigation transactions should be created (defauls to `true`)
+ */
+export function svelteKitRoutingInstrumentation(
+ startTransactionFn: (context: TransactionContext) => T | undefined,
+ startTransactionOnPageLoad: boolean = true,
+ startTransactionOnLocationChange: boolean = true,
+): void {
+ if (startTransactionOnPageLoad) {
+ instrumentPageload(startTransactionFn);
+ }
+
+ if (startTransactionOnLocationChange) {
+ instrumentNavigations(startTransactionFn);
+ }
+}
+
+function instrumentPageload(startTransactionFn: (context: TransactionContext) => Transaction | undefined): void {
+ const initialPath = WINDOW && WINDOW.location && WINDOW.location.pathname;
+
+ const pageloadTransaction = startTransactionFn({
+ name: initialPath,
+ op: 'pageload',
+ description: initialPath,
+ tags: {
+ ...DEFAULT_TAGS,
+ },
+ });
+
+ page.subscribe(page => {
+ if (!page) {
+ return;
+ }
+
+ const routeId = page.route && page.route.id;
+
+ if (pageloadTransaction && routeId) {
+ pageloadTransaction.setName(routeId, 'route');
+ }
+ });
+}
+
+/**
+ * Use the `navigating` store to start a transaction on navigations.
+ */
+function instrumentNavigations(startTransactionFn: (context: TransactionContext) => Transaction | undefined): void {
+ let routingSpan: Span | undefined = undefined;
+ let activeTransaction: Transaction | undefined;
+
+ navigating.subscribe(navigation => {
+ if (!navigation) {
+ // `navigating` emits a 'null' value when the navigation is completed.
+ // So in this case, we can finish the routing span. If the transaction was an IdleTransaction,
+ // it will finish automatically and if it was user-created users also need to finish it.
+ if (routingSpan) {
+ routingSpan.finish();
+ routingSpan = undefined;
+ }
+ return;
+ }
+
+ const routeDestination = navigation.to && navigation.to.route.id;
+ const routeOrigin = navigation.from && navigation.from.route.id;
+
+ if (routeOrigin === routeDestination) {
+ return;
+ }
+
+ activeTransaction = getActiveTransaction();
+
+ if (!activeTransaction) {
+ activeTransaction = startTransactionFn({
+ name: routeDestination || (WINDOW && WINDOW.location && WINDOW.location.pathname),
+ op: 'navigation',
+ metadata: { source: 'route' },
+ tags: {
+ ...DEFAULT_TAGS,
+ },
+ });
+ }
+
+ if (activeTransaction) {
+ if (routingSpan) {
+ // If a routing span is still open from a previous navigation, we finish it.
+ routingSpan.finish();
+ }
+ routingSpan = activeTransaction.startChild({
+ op: 'ui.sveltekit.routing',
+ description: 'SvelteKit Route Change',
+ });
+ activeTransaction.setTag('from', routeOrigin);
+ }
+ });
+}
diff --git a/packages/sveltekit/src/client/sdk.ts b/packages/sveltekit/src/client/sdk.ts
index 50f44bdfa353..9bf1d2cb140b 100644
--- a/packages/sveltekit/src/client/sdk.ts
+++ b/packages/sveltekit/src/client/sdk.ts
@@ -1,17 +1,18 @@
-import { defaultRequestInstrumentationOptions } from '@sentry-internal/tracing';
import { hasTracingEnabled } from '@sentry/core';
import type { BrowserOptions } from '@sentry/svelte';
import { BrowserTracing, configureScope, init as initSvelteSdk } from '@sentry/svelte';
import { addOrUpdateIntegration } from '@sentry/utils';
import { applySdkMetadata } from '../common/metadata';
+import { svelteKitRoutingInstrumentation } from './router';
// Treeshakable guard to remove all code related to tracing
declare const __SENTRY_TRACING__: boolean;
/**
+ * Initialize the client side of the Sentry SvelteKit SDK.
*
- * @param options
+ * @param options Configuration options for the SDK.
*/
export function init(options: BrowserOptions): void {
applySdkMetadata(options, ['sveltekit', 'svelte']);
@@ -33,14 +34,11 @@ function addClientIntegrations(options: BrowserOptions): void {
if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) {
if (hasTracingEnabled(options)) {
const defaultBrowserTracingIntegration = new BrowserTracing({
- tracePropagationTargets: [...defaultRequestInstrumentationOptions.tracePropagationTargets],
- // TODO: Add SvelteKit router instrumentations
- // routingInstrumentation: sveltekitRoutingInstrumentation,
+ routingInstrumentation: svelteKitRoutingInstrumentation,
});
integrations = addOrUpdateIntegration(defaultBrowserTracingIntegration, integrations, {
- // TODO: Add SvelteKit router instrumentations
- // options.routingInstrumentation: sveltekitRoutingInstrumentation,
+ 'options.routingInstrumentation': svelteKitRoutingInstrumentation,
});
}
}
diff --git a/packages/sveltekit/test/client/router.test.ts b/packages/sveltekit/test/client/router.test.ts
new file mode 100644
index 000000000000..a517274ea505
--- /dev/null
+++ b/packages/sveltekit/test/client/router.test.ts
@@ -0,0 +1,139 @@
+/* eslint-disable @typescript-eslint/unbound-method */
+import type { Transaction } from '@sentry/types';
+import { writable } from 'svelte/store';
+import type { SpyInstance } from 'vitest';
+import { vi } from 'vitest';
+
+import { navigating, page } from '$app/stores';
+
+import { svelteKitRoutingInstrumentation } from '../../src/client/router';
+
+// we have to overwrite the global mock from `vitest.setup.ts` here to reset the
+// `navigating` store for each test.
+vi.mock('$app/stores', async () => {
+ return {
+ get navigating() {
+ return navigatingStore;
+ },
+ page: writable(),
+ };
+});
+
+let navigatingStore = writable();
+
+describe('sveltekitRoutingInstrumentation', () => {
+ let returnedTransaction: (Transaction & { returnedTransaction: SpyInstance }) | undefined;
+ const mockedStartTransaction = vi.fn().mockImplementation(txnCtx => {
+ returnedTransaction = {
+ ...txnCtx,
+ setName: vi.fn(),
+ startChild: vi.fn().mockImplementation(ctx => {
+ return { ...mockedRoutingSpan, ...ctx };
+ }),
+ setTag: vi.fn(),
+ };
+ return returnedTransaction;
+ });
+
+ const mockedRoutingSpan = {
+ finish: () => {},
+ };
+
+ const routingSpanFinishSpy = vi.spyOn(mockedRoutingSpan, 'finish');
+
+ beforeEach(() => {
+ navigatingStore = writable();
+ vi.clearAllMocks();
+ });
+
+ it("starts a pageload transaction when it's called with default params", () => {
+ svelteKitRoutingInstrumentation(mockedStartTransaction);
+
+ expect(mockedStartTransaction).toHaveBeenCalledTimes(1);
+ expect(mockedStartTransaction).toHaveBeenCalledWith({
+ name: '/',
+ op: 'pageload',
+ description: '/',
+ tags: {
+ 'routing.instrumentation': '@sentry/sveltekit',
+ },
+ });
+
+ // We emit an update to the `page` store to simulate the SvelteKit router lifecycle
+ // @ts-ignore This is fine because we testUtils/stores.ts defines `page` as a writable store
+ page.set({ route: { id: 'testRoute' } });
+
+ // This should update the transaction name with the parameterized route:
+ expect(returnedTransaction?.setName).toHaveBeenCalledTimes(1);
+ expect(returnedTransaction?.setName).toHaveBeenCalledWith('testRoute', 'route');
+ });
+
+ it("doesn't start a pageload transaction if `startTransactionOnPageLoad` is false", () => {
+ svelteKitRoutingInstrumentation(mockedStartTransaction, false);
+ expect(mockedStartTransaction).toHaveBeenCalledTimes(0);
+ });
+
+ it("doesn't starts a navigation transaction when `startTransactionOnLocationChange` is false", () => {
+ svelteKitRoutingInstrumentation(mockedStartTransaction, false, false);
+
+ // We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle
+ // @ts-ignore This is fine because we testUtils/stores.ts defines `navigating` as a writable store
+ navigating.set(
+ { from: { route: { id: 'testNavigationOrigin' } } },
+ { to: { route: { id: 'testNavigationDestination' } } },
+ );
+
+ // This should update the transaction name with the parameterized route:
+ expect(mockedStartTransaction).toHaveBeenCalledTimes(0);
+ });
+
+ it('starts a navigation transaction when `startTransactionOnLocationChange` is true', () => {
+ svelteKitRoutingInstrumentation(mockedStartTransaction, false, true);
+
+ // We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle
+ // @ts-ignore This is fine because we testUtils/stores.ts defines `navigating` as a writable store
+ navigating.set({
+ from: { route: { id: 'testNavigationOrigin' } },
+ to: { route: { id: 'testNavigationDestination' } },
+ });
+
+ // This should update the transaction name with the parameterized route:
+ expect(mockedStartTransaction).toHaveBeenCalledTimes(1);
+ expect(mockedStartTransaction).toHaveBeenCalledWith({
+ name: 'testNavigationDestination',
+ op: 'navigation',
+ metadata: {
+ source: 'route',
+ },
+ tags: {
+ 'routing.instrumentation': '@sentry/sveltekit',
+ },
+ });
+
+ expect(returnedTransaction?.startChild).toHaveBeenCalledWith({
+ op: 'ui.sveltekit.routing',
+ description: 'SvelteKit Route Change',
+ });
+
+ expect(returnedTransaction?.setTag).toHaveBeenCalledWith('from', 'testNavigationOrigin');
+
+ // We emit `null` here to simulate the end of the navigation lifecycle
+ // @ts-ignore this is fine
+ navigating.set(null);
+
+ expect(routingSpanFinishSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("doesn't start a navigation transaction if navigation origin and destination are equal", () => {
+ svelteKitRoutingInstrumentation(mockedStartTransaction, false, true);
+
+ // We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle
+ // @ts-ignore This is fine because we testUtils/stores.ts defines `navigating` as a writable store
+ navigating.set({
+ from: { route: { id: 'testRoute' } },
+ to: { route: { id: 'testRoute' } },
+ });
+
+ expect(mockedStartTransaction).toHaveBeenCalledTimes(0);
+ });
+});
diff --git a/packages/sveltekit/test/client/sdk.test.ts b/packages/sveltekit/test/client/sdk.test.ts
index 8e404578883d..a8353a73df3e 100644
--- a/packages/sveltekit/test/client/sdk.test.ts
+++ b/packages/sveltekit/test/client/sdk.test.ts
@@ -5,6 +5,7 @@ import { SDK_VERSION, WINDOW } from '@sentry/svelte';
import { vi } from 'vitest';
import { BrowserTracing, init } from '../../src/client';
+import { svelteKitRoutingInstrumentation } from '../../src/client/router';
const svelteInit = vi.spyOn(SentrySvelte, 'init');
@@ -87,6 +88,7 @@ describe('Sentry client SDK', () => {
// This is the closest we can get to unit-testing the `__SENTRY_TRACING__` tree-shaking guard
// IRL, the code to add the integration would most likely be removed by the bundler.
+ // @ts-ignore this is fine in the test
globalThis.__SENTRY_TRACING__ = false;
init({
@@ -100,24 +102,35 @@ describe('Sentry client SDK', () => {
expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' }));
expect(browserTracing).toBeUndefined();
+ // @ts-ignore this is fine in the test
delete globalThis.__SENTRY_TRACING__;
});
- // TODO: this test is only meaningful once we have a routing instrumentation which we always want to add
- // to a user-provided BrowserTracing integration (see NextJS SDK)
- it.skip('Merges the user-provided BrowserTracing integration with the automatically added one', () => {
+ it('Merges a user-provided BrowserTracing integration with the automatically added one', () => {
init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
- integrations: [new BrowserTracing({ tracePropagationTargets: ['myDomain.com'] })],
+ integrations: [
+ new BrowserTracing({ tracePropagationTargets: ['myDomain.com'], startTransactionOnLocationChange: false }),
+ ],
enableTracing: true,
});
const integrationsToInit = svelteInit.mock.calls[0][0].integrations;
- const browserTracing = (getCurrentHub().getClient() as BrowserClient)?.getIntegrationById('BrowserTracing');
+
+ const browserTracing = (getCurrentHub().getClient() as BrowserClient)?.getIntegrationById(
+ 'BrowserTracing',
+ ) as BrowserTracing;
+ const options = browserTracing.options;
expect(integrationsToInit).toContainEqual(expect.objectContaining({ name: 'BrowserTracing' }));
expect(browserTracing).toBeDefined();
- expect((browserTracing as BrowserTracing).options.tracePropagationTargets).toEqual(['myDomain.com']);
+
+ // This shows that the user-configured options are still here
+ expect(options.tracePropagationTargets).toEqual(['myDomain.com']);
+ expect(options.startTransactionOnLocationChange).toBe(false);
+
+ // But we force the routing instrumentation to be ours
+ expect(options.routingInstrumentation).toEqual(svelteKitRoutingInstrumentation);
});
});
});
diff --git a/packages/sveltekit/test/vitest.setup.ts b/packages/sveltekit/test/vitest.setup.ts
new file mode 100644
index 000000000000..48c9b0e33528
--- /dev/null
+++ b/packages/sveltekit/test/vitest.setup.ts
@@ -0,0 +1,13 @@
+import { writable } from 'svelte/store';
+import { vi } from 'vitest';
+
+export function setup() {
+ // mock $app/stores because vitest can't resolve this import from SvelteKit.
+ // Seems like $app/stores is only created at build time of a SvelteKit app.
+ vi.mock('$app/stores', async () => {
+ return {
+ navigating: writable(),
+ page: writable(),
+ };
+ });
+}
diff --git a/packages/sveltekit/vite.config.ts b/packages/sveltekit/vite.config.ts
index f479704b7591..c1e4297e11ea 100644
--- a/packages/sveltekit/vite.config.ts
+++ b/packages/sveltekit/vite.config.ts
@@ -1,3 +1,14 @@
+import type { UserConfig } from 'vitest';
+
import baseConfig from '../../vite/vite.config';
-export default baseConfig;
+export default {
+ ...baseConfig,
+ test: {
+ // test exists, no idea why TS doesn't recognize it
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ...(baseConfig as UserConfig & { test: any }).test,
+ environment: 'jsdom',
+ setupFiles: ['./test/vitest.setup.ts'],
+ },
+};
diff --git a/yarn.lock b/yarn.lock
index 0243a33b9b4b..0407a15e420c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -25270,6 +25270,11 @@ svelte@3.49.0:
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.49.0.tgz#5baee3c672306de1070c3b7888fc2204e36a4029"
integrity sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==
+svelte@^3.44.0:
+ version "3.57.0"
+ resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.57.0.tgz#a3969cfe51f25f2a55e75f7b98dbd02c3af0980b"
+ integrity sha512-WMXEvF+RtAaclw0t3bPDTUe19pplMlfyKDsixbHQYgCWi9+O9VN0kXU1OppzrB9gPAvz4NALuoca2LfW2bOjTQ==
+
svgo@^1.0.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
From d265fe53190a82ffcb5b3c0b207b97e7a36130ec Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Wed, 22 Mar 2023 15:22:32 +0100
Subject: [PATCH 14/34] ref(sveltekit): Rewrite `sentryHandle` using trace func
(#7559)
---
packages/sveltekit/src/server/handle.ts | 67 ++++++++-----------------
1 file changed, 22 insertions(+), 45 deletions(-)
diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts
index 90dda26dac55..aab69c085048 100644
--- a/packages/sveltekit/src/server/handle.ts
+++ b/packages/sveltekit/src/server/handle.ts
@@ -1,11 +1,11 @@
/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
-import { captureException, getCurrentHub, startTransaction } from '@sentry/node';
-import type { Transaction } from '@sentry/types';
+import type { Span } from '@sentry/core';
+import { trace } from '@sentry/core';
+import { captureException } from '@sentry/node';
import {
addExceptionMechanism,
baggageHeaderToDynamicSamplingContext,
extractTraceparentData,
- isThenable,
objectify,
} from '@sentry/utils';
import type { Handle } from '@sveltejs/kit';
@@ -51,53 +51,30 @@ function sendErrorToSentry(e: unknown): unknown {
*/
export const sentryHandle: Handle = ({ event, resolve }) => {
return domain.create().bind(() => {
- let maybePromiseResult;
-
const sentryTraceHeader = event.request.headers.get('sentry-trace');
const baggageHeader = event.request.headers.get('baggage');
const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined;
const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader);
- // transaction could be undefined if hub extensions were not added.
- const transaction: Transaction | undefined = startTransaction({
- op: 'http.server',
- name: `${event.request.method} ${event.route.id}`,
- status: 'ok',
- ...traceparentData,
- metadata: {
- source: 'route',
- dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
- },
- });
-
- getCurrentHub().getScope()?.setSpan(transaction);
-
- try {
- maybePromiseResult = resolve(event);
- } catch (e) {
- transaction?.setStatus('internal_error');
- const sentryError = sendErrorToSentry(e);
- transaction?.finish();
- throw sentryError;
- }
-
- if (isThenable(maybePromiseResult)) {
- Promise.resolve(maybePromiseResult).then(
- response => {
- transaction?.setHttpStatus(response.status);
- transaction?.finish();
- },
- e => {
- transaction?.setStatus('internal_error');
- sendErrorToSentry(e);
- transaction?.finish();
+ return trace(
+ {
+ op: 'http.server',
+ name: `${event.request.method} ${event.route.id}`,
+ status: 'ok',
+ ...traceparentData,
+ metadata: {
+ source: 'route',
+ dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
},
- );
- } else {
- transaction?.setHttpStatus(maybePromiseResult.status);
- transaction?.finish();
- }
-
- return maybePromiseResult;
+ },
+ async (span?: Span) => {
+ const res = await resolve(event);
+ if (span) {
+ span.setHttpStatus(res.status);
+ }
+ return res;
+ },
+ sendErrorToSentry,
+ );
})();
};
From 714a9eb7f9a02a0a63273fce54a7b06e20eceb76 Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Wed, 22 Mar 2023 15:34:24 +0100
Subject: [PATCH 15/34] feat(sveltekit): Add performance monitoring for server
load (#7536)
---
packages/sveltekit/src/server/load.ts | 45 +++++++----
packages/sveltekit/test/server/load.test.ts | 89 ++++++++++++++++++++-
2 files changed, 117 insertions(+), 17 deletions(-)
diff --git a/packages/sveltekit/src/server/load.ts b/packages/sveltekit/src/server/load.ts
index ef0433091a9e..5e773365e4a4 100644
--- a/packages/sveltekit/src/server/load.ts
+++ b/packages/sveltekit/src/server/load.ts
@@ -1,6 +1,14 @@
+/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
+import { trace } from '@sentry/core';
import { captureException } from '@sentry/node';
-import { addExceptionMechanism, isThenable, objectify } from '@sentry/utils';
+import {
+ addExceptionMechanism,
+ baggageHeaderToDynamicSamplingContext,
+ extractTraceparentData,
+ objectify,
+} from '@sentry/utils';
import type { HttpError, ServerLoad } from '@sveltejs/kit';
+import * as domain from 'domain';
function isHttpError(err: unknown): err is HttpError {
return typeof err === 'object' && err !== null && 'status' in err && 'body' in err;
@@ -44,21 +52,30 @@ function sendErrorToSentry(e: unknown): unknown {
export function wrapLoadWithSentry(origLoad: ServerLoad): ServerLoad {
return new Proxy(origLoad, {
apply: (wrappingTarget, thisArg, args: Parameters) => {
- let maybePromiseResult;
+ return domain.create().bind(() => {
+ const [event] = args;
- try {
- maybePromiseResult = wrappingTarget.apply(thisArg, args);
- } catch (e) {
- throw sendErrorToSentry(e);
- }
+ const sentryTraceHeader = event.request.headers.get('sentry-trace');
+ const baggageHeader = event.request.headers.get('baggage');
+ const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined;
+ const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader);
- if (isThenable(maybePromiseResult)) {
- Promise.resolve(maybePromiseResult).then(null, e => {
- sendErrorToSentry(e);
- });
- }
-
- return maybePromiseResult;
+ const routeId = event.route.id;
+ return trace(
+ {
+ op: 'function.sveltekit.load',
+ name: routeId ? routeId : event.url.pathname,
+ status: 'ok',
+ ...traceparentData,
+ metadata: {
+ source: routeId ? 'route' : 'url',
+ dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
+ },
+ },
+ () => wrappingTarget.apply(thisArg, args),
+ sendErrorToSentry,
+ );
+ })();
},
});
}
diff --git a/packages/sveltekit/test/server/load.test.ts b/packages/sveltekit/test/server/load.test.ts
index ec2503b945c4..81b067689093 100644
--- a/packages/sveltekit/test/server/load.test.ts
+++ b/packages/sveltekit/test/server/load.test.ts
@@ -1,3 +1,4 @@
+import { addTracingExtensions } from '@sentry/core';
import { Scope } from '@sentry/node';
import type { ServerLoad } from '@sveltejs/kit';
import { error } from '@sveltejs/kit';
@@ -20,6 +21,19 @@ vi.mock('@sentry/node', async () => {
};
});
+const mockTrace = vi.fn();
+
+vi.mock('@sentry/core', async () => {
+ const original = (await vi.importActual('@sentry/core')) as any;
+ return {
+ ...original,
+ trace: (...args: unknown[]) => {
+ mockTrace(...args);
+ return original.trace(...args);
+ },
+ };
+});
+
const mockAddExceptionMechanism = vi.fn();
vi.mock('@sentry/utils', async () => {
@@ -34,10 +48,42 @@ function getById(_id?: string) {
throw new Error('error');
}
+const MOCK_LOAD_ARGS: any = {
+ params: { id: '123' },
+ route: {
+ id: '/users/[id]',
+ },
+ url: new URL('http://localhost:3000/users/123'),
+ request: {
+ headers: {
+ get: (key: string) => {
+ if (key === 'sentry-trace') {
+ return '1234567890abcdef1234567890abcdef-1234567890abcdef-1';
+ }
+
+ if (key === 'baggage') {
+ return (
+ 'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dogpark,' +
+ 'sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,' +
+ 'sentry-trace_id=1234567890abcdef1234567890abcdef,sentry-sample_rate=1'
+ );
+ }
+
+ return null;
+ },
+ },
+ },
+};
+
+beforeAll(() => {
+ addTracingExtensions();
+});
+
describe('wrapLoadWithSentry', () => {
beforeEach(() => {
mockCaptureException.mockClear();
mockAddExceptionMechanism.mockClear();
+ mockTrace.mockClear();
mockScope = new Scope();
});
@@ -49,12 +95,49 @@ describe('wrapLoadWithSentry', () => {
}
const wrappedLoad = wrapLoadWithSentry(load);
- const res = wrappedLoad({ params: { id: '1' } } as any);
+ const res = wrappedLoad(MOCK_LOAD_ARGS);
await expect(res).rejects.toThrow();
expect(mockCaptureException).toHaveBeenCalledTimes(1);
});
+ it('calls trace function', async () => {
+ async function load({ params }: Parameters[0]): Promise> {
+ return {
+ post: params.id,
+ };
+ }
+
+ const wrappedLoad = wrapLoadWithSentry(load);
+ await wrappedLoad(MOCK_LOAD_ARGS);
+
+ expect(mockTrace).toHaveBeenCalledTimes(1);
+ expect(mockTrace).toHaveBeenCalledWith(
+ {
+ op: 'function.sveltekit.load',
+ name: '/users/[id]',
+ parentSampled: true,
+ parentSpanId: '1234567890abcdef',
+ status: 'ok',
+ traceId: '1234567890abcdef1234567890abcdef',
+ metadata: {
+ dynamicSamplingContext: {
+ environment: 'production',
+ public_key: 'dogsarebadatkeepingsecrets',
+ release: '1.0.0',
+ sample_rate: '1',
+ trace_id: '1234567890abcdef1234567890abcdef',
+ transaction: 'dogpark',
+ user_segment: 'segmentA',
+ },
+ source: 'route',
+ },
+ },
+ expect.any(Function),
+ expect.any(Function),
+ );
+ });
+
describe('with error() helper', () => {
it.each([
// [statusCode, timesCalled]
@@ -75,7 +158,7 @@ describe('wrapLoadWithSentry', () => {
}
const wrappedLoad = wrapLoadWithSentry(load);
- const res = wrappedLoad({ params: { id: '1' } } as any);
+ const res = wrappedLoad(MOCK_LOAD_ARGS);
await expect(res).rejects.toThrow();
expect(mockCaptureException).toHaveBeenCalledTimes(times);
@@ -95,7 +178,7 @@ describe('wrapLoadWithSentry', () => {
}
const wrappedLoad = wrapLoadWithSentry(load);
- const res = wrappedLoad({ params: { id: '1' } } as any);
+ const res = wrappedLoad(MOCK_LOAD_ARGS);
await expect(res).rejects.toThrow();
expect(addEventProcessorSpy).toBeCalledTimes(1);
From 046c0c21d1454fe8365a45fc35efd4014815e5f5 Mon Sep 17 00:00:00 2001
From: Lukas Stracke
Date: Wed, 22 Mar 2023 15:38:19 +0100
Subject: [PATCH 16/34] fix(nextjs): Add tracing extension methods in
`wrapServerComponentWithSentry` (#7567)
Our E2E tests were [failing](https://github.com/getsentry/sentry-javascript/actions/runs/4488942749/jobs/7894376591#step:6:6185) because apparently we don't initialize a client while building a NextJS app. This caused the tracing extension methods to not be added, resulting in `startTransaction` returning `undefined`. Not sure if this is the best way to fix this but it seems to work (h/t @timfish for the fix).
---
packages/nextjs/src/server/wrapServerComponentWithSentry.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/packages/nextjs/src/server/wrapServerComponentWithSentry.ts b/packages/nextjs/src/server/wrapServerComponentWithSentry.ts
index eab32207236c..12ff9ceb5f72 100644
--- a/packages/nextjs/src/server/wrapServerComponentWithSentry.ts
+++ b/packages/nextjs/src/server/wrapServerComponentWithSentry.ts
@@ -1,4 +1,4 @@
-import { captureException, getCurrentHub, startTransaction } from '@sentry/core';
+import { addTracingExtensions, captureException, getCurrentHub, startTransaction } from '@sentry/core';
import { baggageHeaderToDynamicSamplingContext, extractTraceparentData } from '@sentry/utils';
import * as domain from 'domain';
@@ -11,6 +11,8 @@ export function wrapServerComponentWithSentry any>
appDirComponent: F,
context: ServerComponentContext,
): F {
+ addTracingExtensions();
+
const { componentRoute, componentType } = context;
// Even though users may define server components as async functions, for the client bundles
From 09ee30bf068bf1e70971f63042ce873fad98bd8f Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Wed, 22 Mar 2023 16:13:12 +0100
Subject: [PATCH 17/34] feat(sveltekit): Add performance monitoring for client
load (#7537)
---
packages/sveltekit/src/client/load.ts | 39 +++++-----
packages/sveltekit/test/client/load.test.ts | 82 +++++++++++++++++++--
2 files changed, 96 insertions(+), 25 deletions(-)
diff --git a/packages/sveltekit/src/client/load.ts b/packages/sveltekit/src/client/load.ts
index fbaa5f98799f..32f310e3bd2d 100644
--- a/packages/sveltekit/src/client/load.ts
+++ b/packages/sveltekit/src/client/load.ts
@@ -1,6 +1,7 @@
+import { trace } from '@sentry/core';
import { captureException } from '@sentry/svelte';
-import { addExceptionMechanism, isThenable, objectify } from '@sentry/utils';
-import type { ServerLoad } from '@sveltejs/kit';
+import { addExceptionMechanism, objectify } from '@sentry/utils';
+import type { Load } from '@sveltejs/kit';
function sendErrorToSentry(e: unknown): unknown {
// In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
@@ -30,24 +31,24 @@ function sendErrorToSentry(e: unknown): unknown {
*
* @param origLoad SvelteKit user defined load function
*/
-export function wrapLoadWithSentry(origLoad: ServerLoad): ServerLoad {
+export function wrapLoadWithSentry(origLoad: Load): Load {
return new Proxy(origLoad, {
- apply: (wrappingTarget, thisArg, args: Parameters) => {
- let maybePromiseResult;
-
- try {
- maybePromiseResult = wrappingTarget.apply(thisArg, args);
- } catch (e) {
- throw sendErrorToSentry(e);
- }
-
- if (isThenable(maybePromiseResult)) {
- Promise.resolve(maybePromiseResult).then(null, e => {
- sendErrorToSentry(e);
- });
- }
-
- return maybePromiseResult;
+ apply: (wrappingTarget, thisArg, args: Parameters) => {
+ const [event] = args;
+
+ const routeId = event.route.id;
+ return trace(
+ {
+ op: 'function.sveltekit.load',
+ name: routeId ? routeId : event.url.pathname,
+ status: 'ok',
+ metadata: {
+ source: routeId ? 'route' : 'url',
+ },
+ },
+ () => wrappingTarget.apply(thisArg, args),
+ sendErrorToSentry,
+ );
},
});
}
diff --git a/packages/sveltekit/test/client/load.test.ts b/packages/sveltekit/test/client/load.test.ts
index 7cbfd3593c03..f4d18c9f9909 100644
--- a/packages/sveltekit/test/client/load.test.ts
+++ b/packages/sveltekit/test/client/load.test.ts
@@ -1,5 +1,5 @@
-import { Scope } from '@sentry/svelte';
-import type { ServerLoad } from '@sveltejs/kit';
+import { addTracingExtensions, Scope } from '@sentry/svelte';
+import type { Load } from '@sveltejs/kit';
import { vi } from 'vitest';
import { wrapLoadWithSentry } from '../../src/client/load';
@@ -19,6 +19,19 @@ vi.mock('@sentry/svelte', async () => {
};
});
+const mockTrace = vi.fn();
+
+vi.mock('@sentry/core', async () => {
+ const original = (await vi.importActual('@sentry/core')) as any;
+ return {
+ ...original,
+ trace: (...args: unknown[]) => {
+ mockTrace(...args);
+ return original.trace(...args);
+ },
+ };
+});
+
const mockAddExceptionMechanism = vi.fn();
vi.mock('@sentry/utils', async () => {
@@ -33,41 +46,98 @@ function getById(_id?: string) {
throw new Error('error');
}
+const MOCK_LOAD_ARGS: any = {
+ params: { id: '123' },
+ route: {
+ id: '/users/[id]',
+ },
+ url: new URL('http://localhost:3000/users/123'),
+ request: {
+ headers: {
+ get: (key: string) => {
+ if (key === 'sentry-trace') {
+ return '1234567890abcdef1234567890abcdef-1234567890abcdef-1';
+ }
+
+ if (key === 'baggage') {
+ return (
+ 'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dogpark,' +
+ 'sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,' +
+ 'sentry-trace_id=1234567890abcdef1234567890abcdef,sentry-sample_rate=1'
+ );
+ }
+
+ return null;
+ },
+ },
+ },
+};
+
+beforeAll(() => {
+ addTracingExtensions();
+});
+
describe('wrapLoadWithSentry', () => {
beforeEach(() => {
mockCaptureException.mockClear();
mockAddExceptionMechanism.mockClear();
+ mockTrace.mockClear();
mockScope = new Scope();
});
it('calls captureException', async () => {
- async function load({ params }: Parameters[0]): Promise> {
+ async function load({ params }: Parameters[0]): Promise> {
return {
post: getById(params.id),
};
}
const wrappedLoad = wrapLoadWithSentry(load);
- const res = wrappedLoad({ params: { id: '1' } } as any);
+ const res = wrappedLoad(MOCK_LOAD_ARGS);
await expect(res).rejects.toThrow();
expect(mockCaptureException).toHaveBeenCalledTimes(1);
});
+ it('calls trace function', async () => {
+ async function load({ params }: Parameters[0]): Promise> {
+ return {
+ post: params.id,
+ };
+ }
+
+ const wrappedLoad = wrapLoadWithSentry(load);
+ await wrappedLoad(MOCK_LOAD_ARGS);
+
+ expect(mockTrace).toHaveBeenCalledTimes(1);
+ expect(mockTrace).toHaveBeenCalledWith(
+ {
+ op: 'function.sveltekit.load',
+ name: '/users/[id]',
+ status: 'ok',
+ metadata: {
+ source: 'route',
+ },
+ },
+ expect.any(Function),
+ expect.any(Function),
+ );
+ });
+
it('adds an exception mechanism', async () => {
const addEventProcessorSpy = vi.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => {
void callback({}, { event_id: 'fake-event-id' });
return mockScope;
});
- async function load({ params }: Parameters[0]): Promise> {
+ async function load({ params }: Parameters[0]): Promise> {
return {
post: getById(params.id),
};
}
const wrappedLoad = wrapLoadWithSentry(load);
- const res = wrappedLoad({ params: { id: '1' } } as any);
+ const res = wrappedLoad(MOCK_LOAD_ARGS);
await expect(res).rejects.toThrow();
expect(addEventProcessorSpy).toBeCalledTimes(1);
From 8e78e6e5ec3b5894176555ee4e19b3368d64c969 Mon Sep 17 00:00:00 2001
From: Abhijeet Prasad
Date: Wed, 22 Mar 2023 16:38:09 +0100
Subject: [PATCH 18/34] fix(react): Handle case where error.cause already
defined (#7557)
If `error.cause` is already defined, attempt to walk down the error chain to set the `ReactErrorBoundary` error.
---
packages/react/src/errorboundary.tsx | 21 ++++-
packages/react/test/errorboundary.test.tsx | 101 +++++++++++++++++++--
2 files changed, 111 insertions(+), 11 deletions(-)
diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx
index 1553028195a2..96a88cf31e73 100644
--- a/packages/react/src/errorboundary.tsx
+++ b/packages/react/src/errorboundary.tsx
@@ -66,6 +66,25 @@ const INITIAL_STATE = {
eventId: null,
};
+function setCause(error: Error & { cause?: Error }, cause: Error): void {
+ const seenErrors = new WeakMap();
+
+ function recurse(error: Error & { cause?: Error }, cause: Error): void {
+ // If we've already seen the error, there is a recursive loop somewhere in the error's
+ // cause chain. Let's just bail out then to prevent a stack overflow.
+ if (seenErrors.has(error)) {
+ return;
+ }
+ if (error.cause) {
+ seenErrors.set(error, true);
+ return recurse(error.cause, cause);
+ }
+ error.cause = cause;
+ }
+
+ recurse(error, cause);
+}
+
/**
* A ErrorBoundary component that logs errors to Sentry. Requires React >= 16.
* NOTE: If you are a Sentry user, and you are seeing this stack frame, it means the
@@ -93,7 +112,7 @@ class ErrorBoundary extends React.Component;
}
-const TestApp: React.FC = ({ children, ...props }) => {
+interface TestAppProps extends ErrorBoundaryProps {
+ errorComp?: JSX.Element;
+}
+
+const TestApp: React.FC = ({ children, errorComp, ...props }) => {
+ // eslint-disable-next-line no-param-reassign
+ const customErrorComp = errorComp || ;
const [isError, setError] = React.useState(false);
return (
= ({ children, ...props }) => {
}
}}
>
- {isError ? : children}
+ {isError ? customErrorComp : children}
} onError={mockOnError} errorComp={}>
+ children
+ ,
+ );
+
+ expect(mockOnError).toHaveBeenCalledTimes(0);
+ expect(mockCaptureException).toHaveBeenCalledTimes(0);
+
+ const btn = screen.getByTestId('errorBtn');
+ fireEvent.click(btn);
+
+ expect(mockCaptureException).toHaveBeenCalledTimes(1);
+ expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), {
+ contexts: { react: { componentStack: expect.any(String) } },
+ });
+
+ expect(mockOnError.mock.calls[0][0]).toEqual(mockCaptureException.mock.calls[0][0]);
+
+ const thirdError = mockCaptureException.mock.calls[0][0];
+ const secondError = thirdError.cause;
+ const firstError = secondError.cause;
+ const cause = firstError.cause;
+ expect(cause.stack).toEqual(mockCaptureException.mock.calls[0][1].contexts.react.componentStack);
+ expect(cause.name).toContain('React ErrorBoundary');
+ expect(cause.message).toEqual(thirdError.message);
+ });
+
+ it('handles when `error.cause` is recursive', () => {
+ const mockOnError = jest.fn();
+
+ function CustomBam(): JSX.Element {
+ const firstError = new Error('bam');
+ const secondError = new Error('bam2');
+ // @ts-ignore Need to set cause on error
+ firstError.cause = secondError;
+ // @ts-ignore Need to set cause on error
+ secondError.cause = firstError;
+ throw firstError;
+ }
+
+ render(
+ You have hit an error} onError={mockOnError} errorComp={}>
+ children
+ ,
+ );
+
+ expect(mockOnError).toHaveBeenCalledTimes(0);
+ expect(mockCaptureException).toHaveBeenCalledTimes(0);
+
+ const btn = screen.getByTestId('errorBtn');
+ fireEvent.click(btn);
+
+ expect(mockCaptureException).toHaveBeenCalledTimes(1);
+ expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), {
+ contexts: { react: { componentStack: expect.any(String) } },
+ });
+
+ expect(mockOnError.mock.calls[0][0]).toEqual(mockCaptureException.mock.calls[0][0]);
+
+ const error = mockCaptureException.mock.calls[0][0];
+ const cause = error.cause;
+ // We need to make sure that recursive error.cause does not cause infinite loop
+ expect(cause.stack).not.toEqual(mockCaptureException.mock.calls[0][1].contexts.react.componentStack);
+ expect(cause.name).not.toContain('React ErrorBoundary');
+ });
+
it('calls `beforeCapture()` when an error occurs', () => {
const mockBeforeCapture = jest.fn();
From aca5249d06e36d8979838ebf7464eb519e15eed9 Mon Sep 17 00:00:00 2001
From: Francesco Novy
Date: Wed, 22 Mar 2023 16:48:24 +0100
Subject: [PATCH 19/34] ci: Improve flaky test detector performance (#7569)
We now run all tests into a single spawned process, which should hopefully speed this up a bit.
We also reduce the # of repetitions from 100 to 50 to ensure we do not run overtime.
---
.github/workflows/flaky-test-detector.yml | 4 +-
.../scripts/detectFlakyTests.ts | 76 ++++++++++++-------
2 files changed, 50 insertions(+), 30 deletions(-)
diff --git a/.github/workflows/flaky-test-detector.yml b/.github/workflows/flaky-test-detector.yml
index bb414b553abd..9412da630775 100644
--- a/.github/workflows/flaky-test-detector.yml
+++ b/.github/workflows/flaky-test-detector.yml
@@ -77,5 +77,5 @@ jobs:
working-directory: packages/browser-integration-tests
env:
CHANGED_TEST_PATHS: ${{ steps.changed.outputs.browser_integration_files }}
- # Run 100 times when detecting changed test(s), else run all tests 5x
- TEST_RUN_COUNT: ${{ steps.changed.outputs.browser_integration == 'true' && 100 || 5 }}
+ # Run 50 times when detecting changed test(s), else run all tests 5x
+ TEST_RUN_COUNT: ${{ steps.changed.outputs.browser_integration == 'true' && 50 || 5 }}
diff --git a/packages/browser-integration-tests/scripts/detectFlakyTests.ts b/packages/browser-integration-tests/scripts/detectFlakyTests.ts
index 22977fa3ed83..af0a5c86a18e 100644
--- a/packages/browser-integration-tests/scripts/detectFlakyTests.ts
+++ b/packages/browser-integration-tests/scripts/detectFlakyTests.ts
@@ -6,52 +6,72 @@ import { promisify } from 'util';
const exec = promisify(childProcess.exec);
async function run(): Promise {
- let testPaths = getTestPaths();
- let failed = [];
+ let testPaths: string[] = [];
- try {
- const changedPaths: string[] = process.env.CHANGED_TEST_PATHS ? JSON.parse(process.env.CHANGED_TEST_PATHS) : [];
+ const changedPaths: string[] = process.env.CHANGED_TEST_PATHS ? JSON.parse(process.env.CHANGED_TEST_PATHS) : [];
- if (changedPaths.length > 0) {
- console.log(`Detected changed test paths:
+ if (changedPaths.length > 0) {
+ console.log(`Detected changed test paths:
${changedPaths.join('\n')}
`);
- testPaths = testPaths.filter(p => changedPaths.some(changedPath => changedPath.includes(p)));
+ testPaths = getTestPaths().filter(p => changedPaths.some(changedPath => changedPath.includes(p)));
+ if (testPaths.length === 0) {
+ console.log('Could not find matching tests, aborting...');
+ process.exit(1);
}
- } catch {
- console.log('Could not detect changed test paths, running all tests.');
}
const cwd = path.join(__dirname, '../');
const runCount = parseInt(process.env.TEST_RUN_COUNT || '10');
- for (const testPath of testPaths) {
- console.log(`Running test: ${testPath}`);
- const start = Date.now();
+ try {
+ await new Promise((resolve, reject) => {
+ const cp = childProcess.spawn(
+ `yarn playwright test ${
+ testPaths.length ? testPaths.join(' ') : './suites'
+ } --browser='all' --reporter='line' --repeat-each ${runCount}`,
+ { shell: true, cwd },
+ );
+
+ let error: Error | undefined;
+
+ cp.stdout.on('data', data => {
+ console.log(data ? (data as object).toString() : '');
+ });
- try {
- await exec(`yarn playwright test ${testPath} --browser='all' --repeat-each ${runCount}`, {
- cwd,
+ cp.stderr.on('data', data => {
+ console.log(data ? (data as object).toString() : '');
});
- const end = Date.now();
- console.log(` ☑️ Passed ${runCount} times, avg. duration ${Math.ceil((end - start) / runCount)}ms`);
- } catch (error) {
- logError(error);
- failed.push(testPath);
- }
- }
- console.log('');
- console.log('');
+ cp.on('error', e => {
+ console.error(e);
+ error = e;
+ });
- if (failed.length > 0) {
- console.error(`⚠️ ${failed.length} test(s) failed.`);
+ cp.on('close', status => {
+ const err = error || (status !== 0 ? new Error(`Process exited with status ${status}`) : undefined);
+
+ if (err) {
+ reject(err);
+ } else {
+ resolve();
+ }
+ });
+ });
+ } catch (error) {
+ console.log('');
+ console.log('');
+
+ console.error(`⚠️ Some tests failed.`);
+ console.error(error);
process.exit(1);
- } else {
- console.log(`☑️ ${testPaths.length} test(s) passed.`);
}
+
+ console.log('');
+ console.log('');
+ console.log(`☑️ All tests passed.`);
}
function getTestPaths(): string[] {
From 21dd20d28c6f806323d0b31c89d6f5d5937a7f96 Mon Sep 17 00:00:00 2001
From: Francesco Novy
Date: Wed, 22 Mar 2023 16:48:36 +0100
Subject: [PATCH 20/34] feat(replay): Capture fetch body size for replay events
(#7524)
---
.../fetch/contentLengthHeader/test.ts | 32 +++++++-
.../fetch/noContentLengthHeader/test.ts | 32 +++++++-
.../fetch/nonTextBody/test.ts | 33 +++++++-
.../fetch/requestBody/test.ts | 46 ++++++++++-
.../xhr/contentLengthHeader/test.ts | 32 +++++++-
.../xhr/noContentLengthHeader/test.ts | 32 +++++++-
.../xhr/nonTextBody/test.ts | 33 +++++++-
.../xhr/requestBody/test.ts | 47 ++++++++++--
.../suites/replay/privacyInput/test.ts | 60 +++++++++++++--
.../coreHandlers/handleNetworkBreadcrumbs.ts | 73 +++++++++++++++---
.../handleNetworkBreadcrumbs.test.ts | 76 +++++++++++++++++--
11 files changed, 455 insertions(+), 41 deletions(-)
diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/contentLengthHeader/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/contentLengthHeader/test.ts
index 1ffeb360c650..27c429c9be98 100644
--- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/contentLengthHeader/test.ts
+++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/contentLengthHeader/test.ts
@@ -2,7 +2,11 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
-import { shouldSkipReplayTest } from '../../../../../utils/replayHelpers';
+import {
+ getCustomRecordingEvents,
+ shouldSkipReplayTest,
+ waitForReplayRequest,
+} from '../../../../../utils/replayHelpers';
sentryTest('parses response_body_size from Content-Length header if available', async ({ getLocalTestPath, page }) => {
if (shouldSkipReplayTest()) {
@@ -22,7 +26,17 @@ sentryTest('parses response_body_size from Content-Length header if available',
});
});
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
const requestPromise = waitForErrorRequest(page);
+ const replayRequestPromise1 = waitForReplayRequest(page, 0);
+
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
@@ -58,4 +72,20 @@ sentryTest('parses response_body_size from Content-Length header if available',
url: 'http://localhost:7654/foo',
},
});
+
+ const replayReq1 = await replayRequestPromise1;
+ const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
+ expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([
+ {
+ data: {
+ method: 'GET',
+ responseBodySize: 789,
+ statusCode: 200,
+ },
+ description: 'http://localhost:7654/foo',
+ endTimestamp: expect.any(Number),
+ op: 'resource.fetch',
+ startTimestamp: expect.any(Number),
+ },
+ ]);
});
diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/noContentLengthHeader/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/noContentLengthHeader/test.ts
index 8248b4799480..31f8d65bc7e7 100644
--- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/noContentLengthHeader/test.ts
+++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/noContentLengthHeader/test.ts
@@ -2,7 +2,11 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
-import { shouldSkipReplayTest } from '../../../../../utils/replayHelpers';
+import {
+ getCustomRecordingEvents,
+ shouldSkipReplayTest,
+ waitForReplayRequest,
+} from '../../../../../utils/replayHelpers';
sentryTest('does not capture response_body_size without Content-Length header', async ({ getLocalTestPath, page }) => {
if (shouldSkipReplayTest()) {
@@ -22,7 +26,17 @@ sentryTest('does not capture response_body_size without Content-Length header',
});
});
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
const requestPromise = waitForErrorRequest(page);
+ const replayRequestPromise1 = waitForReplayRequest(page, 0);
+
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
@@ -57,4 +71,20 @@ sentryTest('does not capture response_body_size without Content-Length header',
url: 'http://localhost:7654/foo',
},
});
+
+ const replayReq1 = await replayRequestPromise1;
+ const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
+ expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([
+ {
+ data: {
+ method: 'GET',
+ responseBodySize: 29,
+ statusCode: 200,
+ },
+ description: 'http://localhost:7654/foo',
+ endTimestamp: expect.any(Number),
+ op: 'resource.fetch',
+ startTimestamp: expect.any(Number),
+ },
+ ]);
});
diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/nonTextBody/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/nonTextBody/test.ts
index a293df49b366..d2c167110a8a 100644
--- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/nonTextBody/test.ts
+++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/nonTextBody/test.ts
@@ -2,7 +2,11 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
-import { shouldSkipReplayTest } from '../../../../../utils/replayHelpers';
+import {
+ getCustomRecordingEvents,
+ shouldSkipReplayTest,
+ waitForReplayRequest,
+} from '../../../../../utils/replayHelpers';
sentryTest('calculates body sizes for non-string bodies', async ({ getLocalTestPath, page }) => {
if (shouldSkipReplayTest()) {
@@ -19,7 +23,17 @@ sentryTest('calculates body sizes for non-string bodies', async ({ getLocalTestP
});
});
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
const requestPromise = waitForErrorRequest(page);
+ const replayRequestPromise1 = waitForReplayRequest(page, 0);
+
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
@@ -60,4 +74,21 @@ sentryTest('calculates body sizes for non-string bodies', async ({ getLocalTestP
url: 'http://localhost:7654/foo',
},
});
+
+ const replayReq1 = await replayRequestPromise1;
+ const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
+ expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([
+ {
+ data: {
+ method: 'POST',
+ requestBodySize: 26,
+ responseBodySize: 24,
+ statusCode: 200,
+ },
+ description: 'http://localhost:7654/foo',
+ endTimestamp: expect.any(Number),
+ op: 'resource.fetch',
+ startTimestamp: expect.any(Number),
+ },
+ ]);
});
diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/requestBody/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/requestBody/test.ts
index baac9005fd35..0f77394b6e5d 100644
--- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/requestBody/test.ts
+++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/requestBody/test.ts
@@ -2,7 +2,11 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
-import { shouldSkipReplayTest } from '../../../../../utils/replayHelpers';
+import {
+ getCustomRecordingEvents,
+ shouldSkipReplayTest,
+ waitForReplayRequest,
+} from '../../../../../utils/replayHelpers';
sentryTest('captures request_body_size when body is sent', async ({ getLocalTestPath, page }) => {
if (shouldSkipReplayTest()) {
@@ -12,16 +16,23 @@ sentryTest('captures request_body_size when body is sent', async ({ getLocalTest
await page.route('**/foo', route => {
return route.fulfill({
status: 200,
- body: JSON.stringify({
- userNames: ['John', 'Jane'],
- }),
headers: {
'Content-Type': 'application/json',
},
});
});
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
const requestPromise = waitForErrorRequest(page);
+ const replayRequestPromise1 = waitForReplayRequest(page, 0);
+
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
@@ -48,5 +59,32 @@ sentryTest('captures request_body_size when body is sent', async ({ getLocalTest
expect(eventData.exception?.values).toHaveLength(1);
expect(eventData?.breadcrumbs?.length).toBe(1);
+ expect(eventData!.breadcrumbs![0]).toEqual({
+ timestamp: expect.any(Number),
+ category: 'fetch',
+ type: 'http',
+ data: {
+ method: 'POST',
+ request_body_size: 13,
+ status_code: 200,
+ url: 'http://localhost:7654/foo',
+ },
+ });
expect(eventData!.breadcrumbs![0].data!.request_body_size).toEqual(13);
+
+ const replayReq1 = await replayRequestPromise1;
+ const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
+ expect(performanceSpans1.filter(span => span.op === 'resource.fetch')).toEqual([
+ {
+ data: {
+ method: 'POST',
+ requestBodySize: 13,
+ statusCode: 200,
+ },
+ description: 'http://localhost:7654/foo',
+ endTimestamp: expect.any(Number),
+ op: 'resource.fetch',
+ startTimestamp: expect.any(Number),
+ },
+ ]);
});
diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/contentLengthHeader/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/contentLengthHeader/test.ts
index b5f517f77352..4ee170939530 100644
--- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/contentLengthHeader/test.ts
+++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/contentLengthHeader/test.ts
@@ -2,7 +2,11 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
-import { shouldSkipReplayTest } from '../../../../../utils/replayHelpers';
+import {
+ getCustomRecordingEvents,
+ shouldSkipReplayTest,
+ waitForReplayRequest,
+} from '../../../../../utils/replayHelpers';
sentryTest(
'parses response_body_size from Content-Length header if available',
@@ -25,7 +29,17 @@ sentryTest(
});
});
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
const requestPromise = waitForErrorRequest(page);
+ const replayRequestPromise1 = waitForReplayRequest(page, 0);
+
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
@@ -65,5 +79,21 @@ sentryTest(
url: 'http://localhost:7654/foo',
},
});
+
+ const replayReq1 = await replayRequestPromise1;
+ const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
+ expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([
+ {
+ data: {
+ method: 'GET',
+ responseBodySize: 789,
+ statusCode: 200,
+ },
+ description: 'http://localhost:7654/foo',
+ endTimestamp: expect.any(Number),
+ op: 'resource.xhr',
+ startTimestamp: expect.any(Number),
+ },
+ ]);
},
);
diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/noContentLengthHeader/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/noContentLengthHeader/test.ts
index 9ea10831afab..9a9bd633c71f 100644
--- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/noContentLengthHeader/test.ts
+++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/noContentLengthHeader/test.ts
@@ -2,7 +2,11 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
-import { shouldSkipReplayTest } from '../../../../../utils/replayHelpers';
+import {
+ getCustomRecordingEvents,
+ shouldSkipReplayTest,
+ waitForReplayRequest,
+} from '../../../../../utils/replayHelpers';
sentryTest(
'captures response_body_size without Content-Length header',
@@ -25,7 +29,17 @@ sentryTest(
});
});
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
const requestPromise = waitForErrorRequest(page);
+ const replayRequestPromise1 = waitForReplayRequest(page, 0);
+
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
@@ -65,5 +79,21 @@ sentryTest(
url: 'http://localhost:7654/foo',
},
});
+
+ const replayReq1 = await replayRequestPromise1;
+ const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
+ expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([
+ {
+ data: {
+ method: 'GET',
+ responseBodySize: 29,
+ statusCode: 200,
+ },
+ description: 'http://localhost:7654/foo',
+ endTimestamp: expect.any(Number),
+ op: 'resource.xhr',
+ startTimestamp: expect.any(Number),
+ },
+ ]);
},
);
diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/nonTextBody/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/nonTextBody/test.ts
index 5142f2e6be82..0210283fea60 100644
--- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/nonTextBody/test.ts
+++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/nonTextBody/test.ts
@@ -2,7 +2,11 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
-import { shouldSkipReplayTest } from '../../../../../utils/replayHelpers';
+import {
+ getCustomRecordingEvents,
+ shouldSkipReplayTest,
+ waitForReplayRequest,
+} from '../../../../../utils/replayHelpers';
sentryTest('calculates body sizes for non-string bodies', async ({ getLocalTestPath, page, browserName }) => {
// These are a bit flaky on non-chromium browsers
@@ -20,7 +24,17 @@ sentryTest('calculates body sizes for non-string bodies', async ({ getLocalTestP
});
});
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
const requestPromise = waitForErrorRequest(page);
+ const replayRequestPromise1 = waitForReplayRequest(page, 0);
+
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
@@ -63,4 +77,21 @@ sentryTest('calculates body sizes for non-string bodies', async ({ getLocalTestP
url: 'http://localhost:7654/foo',
},
});
+
+ const replayReq1 = await replayRequestPromise1;
+ const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
+ expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([
+ {
+ data: {
+ method: 'POST',
+ requestBodySize: 26,
+ responseBodySize: 24,
+ statusCode: 200,
+ },
+ description: 'http://localhost:7654/foo',
+ endTimestamp: expect.any(Number),
+ op: 'resource.xhr',
+ startTimestamp: expect.any(Number),
+ },
+ ]);
});
diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/requestBody/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/requestBody/test.ts
index fd3cc426f9fd..470fe57c51ba 100644
--- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/requestBody/test.ts
+++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/requestBody/test.ts
@@ -2,7 +2,11 @@ import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequest } from '../../../../../utils/helpers';
-import { shouldSkipReplayTest } from '../../../../../utils/replayHelpers';
+import {
+ getCustomRecordingEvents,
+ shouldSkipReplayTest,
+ waitForReplayRequest,
+} from '../../../../../utils/replayHelpers';
sentryTest('captures request_body_size when body is sent', async ({ getLocalTestPath, page, browserName }) => {
// These are a bit flaky on non-chromium browsers
@@ -13,9 +17,6 @@ sentryTest('captures request_body_size when body is sent', async ({ getLocalTest
await page.route('**/foo', route => {
return route.fulfill({
status: 200,
- body: JSON.stringify({
- userNames: ['John', 'Jane'],
- }),
headers: {
'Content-Type': 'application/json',
'Content-Length': '',
@@ -23,7 +24,17 @@ sentryTest('captures request_body_size when body is sent', async ({ getLocalTest
});
});
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
const requestPromise = waitForErrorRequest(page);
+ const replayRequestPromise1 = waitForReplayRequest(page, 0);
+
const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
@@ -52,5 +63,31 @@ sentryTest('captures request_body_size when body is sent', async ({ getLocalTest
expect(eventData.exception?.values).toHaveLength(1);
expect(eventData?.breadcrumbs?.length).toBe(1);
- expect(eventData!.breadcrumbs![0].data!.request_body_size).toEqual(13);
+ expect(eventData!.breadcrumbs![0]).toEqual({
+ timestamp: expect.any(Number),
+ category: 'xhr',
+ type: 'http',
+ data: {
+ method: 'POST',
+ request_body_size: 13,
+ status_code: 200,
+ url: 'http://localhost:7654/foo',
+ },
+ });
+
+ const replayReq1 = await replayRequestPromise1;
+ const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(replayReq1);
+ expect(performanceSpans1.filter(span => span.op === 'resource.xhr')).toEqual([
+ {
+ data: {
+ method: 'POST',
+ requestBodySize: 13,
+ statusCode: 200,
+ },
+ description: 'http://localhost:7654/foo',
+ endTimestamp: expect.any(Number),
+ op: 'resource.xhr',
+ startTimestamp: expect.any(Number),
+ },
+ ]);
});
diff --git a/packages/browser-integration-tests/suites/replay/privacyInput/test.ts b/packages/browser-integration-tests/suites/replay/privacyInput/test.ts
index f95e857d5637..dc071b9bf487 100644
--- a/packages/browser-integration-tests/suites/replay/privacyInput/test.ts
+++ b/packages/browser-integration-tests/suites/replay/privacyInput/test.ts
@@ -24,10 +24,34 @@ sentryTest(
sentryTest.skip();
}
+ // We want to ensure to check the correct event payloads
+ const inputMutationSegmentIds: number[] = [];
const reqPromise0 = waitForReplayRequest(page, 0);
- const reqPromise1 = waitForReplayRequest(page, 1);
- const reqPromise2 = waitForReplayRequest(page, 2);
- const reqPromise3 = waitForReplayRequest(page, 3);
+ const reqPromise1 = waitForReplayRequest(page, (event, res) => {
+ const check = inputMutationSegmentIds.length === 0 && getIncrementalRecordingSnapshots(res).some(isInputMutation);
+
+ if (check) {
+ inputMutationSegmentIds.push(event.segment_id);
+ }
+
+ return check;
+ });
+ const reqPromise2 = waitForReplayRequest(page, (event, res) => {
+ const check =
+ inputMutationSegmentIds.length === 1 &&
+ inputMutationSegmentIds[0] < event.segment_id &&
+ getIncrementalRecordingSnapshots(res).some(isInputMutation);
+
+ if (check) {
+ inputMutationSegmentIds.push(event.segment_id);
+ }
+
+ return check;
+ });
+ const reqPromise3 = waitForReplayRequest(page, event => {
+ // This one should not have any input mutations
+ return inputMutationSegmentIds.length === 2 && inputMutationSegmentIds[1] < event.segment_id;
+ });
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
return route.fulfill({
@@ -72,10 +96,34 @@ sentryTest(
sentryTest.skip();
}
+ // We want to ensure to check the correct event payloads
+ const inputMutationSegmentIds: number[] = [];
const reqPromise0 = waitForReplayRequest(page, 0);
- const reqPromise1 = waitForReplayRequest(page, 1);
- const reqPromise2 = waitForReplayRequest(page, 2);
- const reqPromise3 = waitForReplayRequest(page, 3);
+ const reqPromise1 = waitForReplayRequest(page, (event, res) => {
+ const check = inputMutationSegmentIds.length === 0 && getIncrementalRecordingSnapshots(res).some(isInputMutation);
+
+ if (check) {
+ inputMutationSegmentIds.push(event.segment_id);
+ }
+
+ return check;
+ });
+ const reqPromise2 = waitForReplayRequest(page, (event, res) => {
+ const check =
+ inputMutationSegmentIds.length === 1 &&
+ inputMutationSegmentIds[0] < event.segment_id &&
+ getIncrementalRecordingSnapshots(res).some(isInputMutation);
+
+ if (check) {
+ inputMutationSegmentIds.push(event.segment_id);
+ }
+
+ return check;
+ });
+ const reqPromise3 = waitForReplayRequest(page, event => {
+ // This one should not have any input mutations
+ return inputMutationSegmentIds.length === 2 && inputMutationSegmentIds[1] < event.segment_id;
+ });
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
return route.fulfill({
diff --git a/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts
index 0ee72f9edc9c..e9ad5d9ea209 100644
--- a/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts
+++ b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts
@@ -51,7 +51,7 @@ export function handleNetworkBreadcrumbs(replay: ReplayContainer): void {
};
if (client && client.on) {
- client.on('beforeAddBreadcrumb', (breadcrumb, hint) => handleNetworkBreadcrumb(options, breadcrumb, hint));
+ client.on('beforeAddBreadcrumb', (breadcrumb, hint) => beforeAddNetworkBreadcrumb(options, breadcrumb, hint));
} else {
// Fallback behavior
addInstrumentationHandler('fetch', handleFetchSpanListener(replay));
@@ -63,7 +63,7 @@ export function handleNetworkBreadcrumbs(replay: ReplayContainer): void {
}
/** just exported for tests */
-export function handleNetworkBreadcrumb(
+export function beforeAddNetworkBreadcrumb(
options: ExtendedNetworkBreadcrumbsOptions,
breadcrumb: Breadcrumb,
hint?: BreadcrumbHint,
@@ -74,27 +74,76 @@ export function handleNetworkBreadcrumb(
try {
if (_isXhrBreadcrumb(breadcrumb) && _isXhrHint(hint)) {
- // Enriches the breadcrumb overall
- _enrichXhrBreadcrumb(breadcrumb, hint, options);
-
- // Create a replay performance entry from this breadcrumb
- const result = _makeNetworkReplayBreadcrumb('resource.xhr', breadcrumb, hint);
- addNetworkBreadcrumb(options.replay, result);
+ _handleXhrBreadcrumb(breadcrumb, hint, options);
}
if (_isFetchBreadcrumb(breadcrumb) && _isFetchHint(hint)) {
- // Enriches the breadcrumb overall
+ // This has to be sync, as we need to ensure the breadcrumb is enriched in the same tick
+ // Because the hook runs synchronously, and the breadcrumb is afterwards passed on
+ // So any async mutations to it will not be reflected in the final breadcrumb
_enrichFetchBreadcrumb(breadcrumb, hint, options);
- // Create a replay performance entry from this breadcrumb
- const result = _makeNetworkReplayBreadcrumb('resource.fetch', breadcrumb, hint);
- addNetworkBreadcrumb(options.replay, result);
+ void _handleFetchBreadcrumb(breadcrumb, hint, options);
}
} catch (e) {
__DEBUG_BUILD__ && logger.warn('Error when enriching network breadcrumb');
}
}
+function _handleXhrBreadcrumb(
+ breadcrumb: Breadcrumb & { data: XhrBreadcrumbData },
+ hint: XhrHint,
+ options: ExtendedNetworkBreadcrumbsOptions,
+): void {
+ // Enriches the breadcrumb overall
+ _enrichXhrBreadcrumb(breadcrumb, hint, options);
+
+ // Create a replay performance entry from this breadcrumb
+ const result = _makeNetworkReplayBreadcrumb('resource.xhr', breadcrumb, hint);
+ addNetworkBreadcrumb(options.replay, result);
+}
+
+async function _handleFetchBreadcrumb(
+ breadcrumb: Breadcrumb & { data: FetchBreadcrumbData },
+ hint: FetchHint,
+ options: ExtendedNetworkBreadcrumbsOptions,
+): Promise {
+ const fullBreadcrumb = await _parseFetchResponse(breadcrumb, hint, options);
+
+ // Create a replay performance entry from this breadcrumb
+ const result = _makeNetworkReplayBreadcrumb('resource.fetch', fullBreadcrumb, hint);
+ addNetworkBreadcrumb(options.replay, result);
+}
+
+// This does async operations on the breadcrumb for replay
+async function _parseFetchResponse(
+ breadcrumb: Breadcrumb & { data: FetchBreadcrumbData },
+ hint: FetchBreadcrumbHint,
+ options: ExtendedNetworkBreadcrumbsOptions,
+): Promise {
+ if (breadcrumb.data.response_body_size || !hint.response) {
+ return breadcrumb;
+ }
+
+ // If no Content-Length header exists, we try to get the size from the response body
+ try {
+ // We have to clone this, as the body can only be read once
+ const response = (hint.response as Response).clone();
+ const body = await response.text();
+
+ if (body.length) {
+ return {
+ ...breadcrumb,
+ data: { ...breadcrumb.data, response_body_size: getBodySize(body, options.textEncoder) },
+ };
+ }
+ } catch {
+ // just ignore if something fails here
+ }
+
+ return breadcrumb;
+}
+
function _makeNetworkReplayBreadcrumb(
type: string,
breadcrumb: Breadcrumb & { data: FetchBreadcrumbData | XhrBreadcrumbData },
diff --git a/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts b/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts
index d0a7b5cedca1..1a1c5ac13d6b 100644
--- a/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts
+++ b/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts
@@ -9,8 +9,8 @@ import { TextEncoder } from 'util';
import { BASE_TIMESTAMP } from '../..';
import {
+ beforeAddNetworkBreadcrumb,
getBodySize,
- handleNetworkBreadcrumb,
parseContentSizeHeader,
} from '../../../src/coreHandlers/handleNetworkBreadcrumbs';
import type { EventBufferArray } from '../../../src/eventBuffer/EventBufferArray';
@@ -78,7 +78,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
});
});
- describe('handleNetworkBreadcrumb()', () => {
+ describe('beforeAddNetworkBreadcrumb()', () => {
let options: {
replay: ReplayContainer;
textEncoder: TextEncoderInternal;
@@ -98,7 +98,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
it('ignores breadcrumb without data', () => {
const breadcrumb: Breadcrumb = {};
const hint: BreadcrumbHint = {};
- handleNetworkBreadcrumb(options, breadcrumb, hint);
+ beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
expect(breadcrumb).toEqual({});
expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([]);
@@ -110,7 +110,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
data: {},
};
const hint: BreadcrumbHint = {};
- handleNetworkBreadcrumb(options, breadcrumb, hint);
+ beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
expect(breadcrumb).toEqual({
category: 'foo',
@@ -138,7 +138,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
startTimestamp: BASE_TIMESTAMP + 1000,
endTimestamp: BASE_TIMESTAMP + 2000,
};
- handleNetworkBreadcrumb(options, breadcrumb, hint);
+ beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
expect(breadcrumb).toEqual({
category: 'xhr',
@@ -192,7 +192,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
startTimestamp: BASE_TIMESTAMP + 1000,
endTimestamp: BASE_TIMESTAMP + 2000,
};
- handleNetworkBreadcrumb(options, breadcrumb, hint);
+ beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
expect(breadcrumb).toEqual({
category: 'xhr',
@@ -246,7 +246,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
startTimestamp: BASE_TIMESTAMP + 1000,
endTimestamp: BASE_TIMESTAMP + 2000,
};
- handleNetworkBreadcrumb(options, breadcrumb, hint);
+ beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
expect(breadcrumb).toEqual({
category: 'fetch',
@@ -260,6 +260,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
});
jest.runAllTimers();
+ await Promise.resolve();
expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([
{
@@ -305,7 +306,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
startTimestamp: BASE_TIMESTAMP + 1000,
endTimestamp: BASE_TIMESTAMP + 2000,
};
- handleNetworkBreadcrumb(options, breadcrumb, hint);
+ beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
expect(breadcrumb).toEqual({
category: 'fetch',
@@ -316,6 +317,7 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
});
jest.runAllTimers();
+ await Promise.resolve();
expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([
{
@@ -336,5 +338,63 @@ describe('Unit | coreHandlers | handleNetworkBreadcrumbs', () => {
},
]);
});
+
+ it('parses fetch response body if necessary', async () => {
+ const breadcrumb: Breadcrumb = {
+ category: 'fetch',
+ data: {
+ url: 'https://example.com',
+ status_code: 200,
+ },
+ };
+
+ const mockResponse = {
+ headers: {
+ get: () => '',
+ },
+ clone: () => mockResponse,
+ text: () => Promise.resolve('test response'),
+ } as unknown as Response;
+
+ const hint: FetchBreadcrumbHint = {
+ input: [],
+ response: mockResponse,
+ startTimestamp: BASE_TIMESTAMP + 1000,
+ endTimestamp: BASE_TIMESTAMP + 2000,
+ };
+ beforeAddNetworkBreadcrumb(options, breadcrumb, hint);
+
+ expect(breadcrumb).toEqual({
+ category: 'fetch',
+ data: {
+ status_code: 200,
+ url: 'https://example.com',
+ },
+ });
+
+ await Promise.resolve();
+ jest.runAllTimers();
+ await Promise.resolve();
+
+ expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([
+ {
+ type: 5,
+ timestamp: (BASE_TIMESTAMP + 1000) / 1000,
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ data: {
+ statusCode: 200,
+ responseBodySize: 13,
+ },
+ description: 'https://example.com',
+ endTimestamp: (BASE_TIMESTAMP + 2000) / 1000,
+ op: 'resource.fetch',
+ startTimestamp: (BASE_TIMESTAMP + 1000) / 1000,
+ },
+ },
+ },
+ ]);
+ });
});
});
From 0e3552d82a03a7e6e88f1f1eff02c6a37c6c4740 Mon Sep 17 00:00:00 2001
From: Francesco Novy
Date: Wed, 22 Mar 2023 16:53:20 +0100
Subject: [PATCH 21/34] build(cdn): Ensure ES5 bundles do not use non-ES5 code
(#7550)
---
.github/workflows/build.yml | 4 +-
package.json | 2 +
packages/browser/package.json | 1 +
packages/eslint-config-sdk/src/index.js | 3 +
packages/eslint-plugin-sdk/src/index.js | 1 +
.../src/rules/no-unsupported-es6-methods.js | 35 ++++
packages/node/.eslintrc.js | 1 +
packages/overhead-metrics/.eslintrc.cjs | 1 +
packages/replay/.eslintrc.js | 17 +-
packages/svelte/.eslintrc.js | 3 +
packages/tracing/package.json | 1 +
packages/vue/src/components.ts | 1 +
rollup/bundleHelpers.js | 14 +-
rollup/plugins/bundlePlugins.js | 8 +
rollup/polyfills/es5.js | 41 +++++
yarn.lock | 150 +++++++++++++++++-
16 files changed, 255 insertions(+), 28 deletions(-)
create mode 100644 packages/eslint-plugin-sdk/src/rules/no-unsupported-es6-methods.js
create mode 100644 rollup/polyfills/es5.js
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f5d0364b7cda..6004a046f126 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -321,8 +321,10 @@ jobs:
uses: ./.github/actions/restore-cache
env:
DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }}
- - name: Run linter
+ - name: Lint source files
run: yarn lint
+ - name: Validate ES5 builds
+ run: yarn validate:es5
job_circular_dep_check:
name: Circular Dependency Check
diff --git a/package.json b/package.json
index 2296a10dfede..e6a54d7d032c 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"link:yarn": "lerna exec yarn link",
"lint": "lerna run lint",
"lint:eslint": "lerna run lint:eslint",
+ "validate:es5": "lerna run validate:es5",
"postpublish": "lerna run --stream --concurrency 1 postpublish",
"test": "lerna run --ignore @sentry-internal/* test",
"test:unit": "lerna run --ignore @sentry-internal/* test:unit",
@@ -89,6 +90,7 @@
"chai": "^4.1.2",
"codecov": "^3.6.5",
"deepmerge": "^4.2.2",
+ "es-check": "7.1.0",
"eslint": "7.32.0",
"jest": "^27.5.1",
"jest-environment-node": "^27.5.1",
diff --git a/packages/browser/package.json b/packages/browser/package.json
index 5d93e763b309..fb2162449405 100644
--- a/packages/browser/package.json
+++ b/packages/browser/package.json
@@ -66,6 +66,7 @@
"lint": "run-s lint:prettier lint:eslint",
"lint:eslint": "eslint . --format stylish",
"lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"",
+ "validate:es5": "es-check es5 build/bundles/bundle.es5.js",
"size:check": "run-p size:check:es5 size:check:es6",
"size:check:es5": "cat build/bundles/bundle.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print \"ES5: \",$1,\"kB\";}'",
"size:check:es6": "cat build/bundles/bundle.es6.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print \"ES6: \",$1,\"kB\";}'",
diff --git a/packages/eslint-config-sdk/src/index.js b/packages/eslint-config-sdk/src/index.js
index d09be9e2e67d..05ec68cff509 100644
--- a/packages/eslint-config-sdk/src/index.js
+++ b/packages/eslint-config-sdk/src/index.js
@@ -161,6 +161,9 @@ module.exports = {
// All imports should be accounted for
'import/no-extraneous-dependencies': 'error',
+
+ // Do not allow usage of functions we do not polyfill for ES5
+ '@sentry-internal/sdk/no-unsupported-es6-methods': 'error',
},
},
{
diff --git a/packages/eslint-plugin-sdk/src/index.js b/packages/eslint-plugin-sdk/src/index.js
index 31ac785abf5e..31d8e932d904 100644
--- a/packages/eslint-plugin-sdk/src/index.js
+++ b/packages/eslint-plugin-sdk/src/index.js
@@ -13,5 +13,6 @@ module.exports = {
'no-optional-chaining': require('./rules/no-optional-chaining'),
'no-nullish-coalescing': require('./rules/no-nullish-coalescing'),
'no-eq-empty': require('./rules/no-eq-empty'),
+ 'no-unsupported-es6-methods': require('./rules/no-unsupported-es6-methods'),
},
};
diff --git a/packages/eslint-plugin-sdk/src/rules/no-unsupported-es6-methods.js b/packages/eslint-plugin-sdk/src/rules/no-unsupported-es6-methods.js
new file mode 100644
index 000000000000..85d32fb20e66
--- /dev/null
+++ b/packages/eslint-plugin-sdk/src/rules/no-unsupported-es6-methods.js
@@ -0,0 +1,35 @@
+'use strict';
+
+/**
+ * Taken and adapted from https://github.com/nkt/eslint-plugin-es5/blob/master/src/rules/no-es6-methods.js
+ */
+
+module.exports = {
+ meta: {
+ docs: {
+ description: 'Forbid methods added in ES6 which are not polyfilled by Sentry.',
+ },
+ schema: [],
+ },
+ create(context) {
+ return {
+ CallExpression(node) {
+ if (!node.callee || !node.callee.property) {
+ return;
+ }
+ const functionName = node.callee.property.name;
+
+ const es6ArrayFunctions = ['copyWithin', 'values', 'fill'];
+ const es6StringFunctions = ['repeat'];
+
+ const es6Functions = [].concat(es6ArrayFunctions, es6StringFunctions);
+ if (es6Functions.indexOf(functionName) > -1) {
+ context.report({
+ node: node.callee.property,
+ message: `ES6 methods not allowed: ${functionName}`,
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/packages/node/.eslintrc.js b/packages/node/.eslintrc.js
index e15b119aa31e..8e4b96002ae0 100644
--- a/packages/node/.eslintrc.js
+++ b/packages/node/.eslintrc.js
@@ -6,5 +6,6 @@ module.exports = {
rules: {
'@sentry-internal/sdk/no-optional-chaining': 'off',
'@sentry-internal/sdk/no-nullish-coalescing': 'off',
+ '@sentry-internal/sdk/no-unsupported-es6-methods': 'off',
},
};
diff --git a/packages/overhead-metrics/.eslintrc.cjs b/packages/overhead-metrics/.eslintrc.cjs
index 1cbdd66daa88..8046df7df92c 100644
--- a/packages/overhead-metrics/.eslintrc.cjs
+++ b/packages/overhead-metrics/.eslintrc.cjs
@@ -10,6 +10,7 @@ module.exports = {
'import/no-unresolved': 'off',
'@sentry-internal/sdk/no-optional-chaining': 'off',
'@sentry-internal/sdk/no-nullish-coalescing': 'off',
+ '@sentry-internal/sdk/no-unsupported-es6-methods': 'off',
'jsdoc/require-jsdoc': 'off',
},
},
diff --git a/packages/replay/.eslintrc.js b/packages/replay/.eslintrc.js
index e4101e557b26..da006cf432a2 100644
--- a/packages/replay/.eslintrc.js
+++ b/packages/replay/.eslintrc.js
@@ -7,21 +7,8 @@ module.exports = {
extends: ['../../.eslintrc.js'],
overrides: [
{
- files: ['worker/**/*.ts'],
- parserOptions: {
- // TODO: figure out if we need a worker-specific tsconfig
- project: ['tsconfig.worker.json'],
- },
- rules: {
- // We cannot use backticks, as that conflicts with the stringified worker
- 'prefer-template': 'off',
- },
- },
- {
- files: ['src/worker/**/*.js'],
- parserOptions: {
- sourceType: 'module',
- },
+ files: ['src/**/*.ts'],
+ rules: {},
},
{
files: ['jest.setup.ts', 'jest.config.ts'],
diff --git a/packages/svelte/.eslintrc.js b/packages/svelte/.eslintrc.js
index 46d8d10cc538..0714feabf8d4 100644
--- a/packages/svelte/.eslintrc.js
+++ b/packages/svelte/.eslintrc.js
@@ -3,4 +3,7 @@ module.exports = {
browser: true,
},
extends: ['../../.eslintrc.js'],
+ rules: {
+ '@sentry-internal/sdk/no-unsupported-es6-methods': 'off',
+ },
};
diff --git a/packages/tracing/package.json b/packages/tracing/package.json
index ef6d6d8f060c..24ec8fbb74d5 100644
--- a/packages/tracing/package.json
+++ b/packages/tracing/package.json
@@ -46,6 +46,7 @@
"lint": "run-s lint:prettier lint:eslint",
"lint:eslint": "eslint . --format stylish",
"lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"",
+ "validate:es5": "es-check es5 build/bundles/bundle.tracing.es5.js",
"test:unit": "jest",
"test": "jest",
"test:watch": "jest --watch",
diff --git a/packages/vue/src/components.ts b/packages/vue/src/components.ts
index 3de2c9baa7af..22ef3e530384 100644
--- a/packages/vue/src/components.ts
+++ b/packages/vue/src/components.ts
@@ -9,6 +9,7 @@ const ANONYMOUS_COMPONENT_NAME = '';
const repeat = (str: string, n: number): string => {
// string.repeat() is not supported by IE11, we fall back to just using the string in that case
+ // eslint-disable-next-line @sentry-internal/sdk/no-unsupported-es6-methods
return str.repeat ? str.repeat(n) : str;
};
diff --git a/rollup/bundleHelpers.js b/rollup/bundleHelpers.js
index 54cd0528f271..b2d25b58d248 100644
--- a/rollup/bundleHelpers.js
+++ b/rollup/bundleHelpers.js
@@ -17,6 +17,7 @@ import {
makeTerserPlugin,
makeTSPlugin,
makeSetSDKSourcePlugin,
+ getEs5Polyfills,
} from './plugins/index.js';
import { mergePlugins } from './utils';
@@ -25,6 +26,8 @@ const BUNDLE_VARIANTS = ['.js', '.min.js', '.debug.min.js'];
export function makeBaseBundleConfig(options) {
const { bundleType, entrypoints, jsVersion, licenseTitle, outputFileBase, packageSpecificConfig } = options;
+ const isEs5 = jsVersion.toLowerCase() === 'es5';
+
const nodeResolvePlugin = makeNodeResolvePlugin();
const sucrasePlugin = makeSucrasePlugin();
const cleanupPlugin = makeCleanupPlugin();
@@ -42,6 +45,10 @@ export function makeBaseBundleConfig(options) {
output: {
format: 'iife',
name: 'Sentry',
+ outro: () => {
+ // Add polyfills for ES6 array/string methods at the end of the bundle
+ return isEs5 ? getEs5Polyfills() : '';
+ },
},
context: 'window',
plugins: [markAsBrowserBuildPlugin],
@@ -101,10 +108,9 @@ export function makeBaseBundleConfig(options) {
strict: false,
esModule: false,
},
- plugins:
- jsVersion === 'es5'
- ? [tsPlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin]
- : [sucrasePlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin],
+ plugins: isEs5
+ ? [tsPlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin]
+ : [sucrasePlugin, nodeResolvePlugin, cleanupPlugin, licensePlugin],
treeshake: 'smallest',
};
diff --git a/rollup/plugins/bundlePlugins.js b/rollup/plugins/bundlePlugins.js
index 81c41d310950..80f5123ca371 100644
--- a/rollup/plugins/bundlePlugins.js
+++ b/rollup/plugins/bundlePlugins.js
@@ -8,6 +8,9 @@
* Typescript plugin docs: https://github.com/ezolenko/rollup-plugin-typescript2
*/
+import * as fs from 'fs';
+import * as path from 'path';
+
import commonjs from '@rollup/plugin-commonjs';
import deepMerge from 'deepmerge';
import license from 'rollup-plugin-license';
@@ -38,6 +41,11 @@ export function makeLicensePlugin(title) {
return plugin;
}
+export function getEs5Polyfills() {
+ // Note: __dirname resolves to e.g. packages/browser or packages/tracing
+ return fs.readFileSync(path.join(__dirname, '../../rollup/polyfills/es5.js'), 'utf-8');
+}
+
/**
* Create a plugin to set the value of the `__SENTRY_DEBUG__` magic string.
*
diff --git a/rollup/polyfills/es5.js b/rollup/polyfills/es5.js
new file mode 100644
index 000000000000..54bd46e62cff
--- /dev/null
+++ b/rollup/polyfills/es5.js
@@ -0,0 +1,41 @@
+// Sentry ES5 polyfills
+if (!('includes' in Array.prototype)) {
+ Array.prototype.includes = function (searchElement) {
+ return this.indexOf(searchElement) > -1;
+ };
+}
+if (!('find' in Array.prototype)) {
+ Array.prototype.find = function (callback) {
+ for (var i = 0; i < this.length; i++) {
+ if (callback(this[i])) {
+ return this[i];
+ }
+ }
+ };
+}
+if (!('findIndex' in Array.prototype)) {
+ Array.prototype.findIndex = function (callback) {
+ for (var i = 0; i < this.length; i++) {
+ if (callback(this[i])) {
+ return i;
+ }
+ }
+ return -1;
+ };
+}
+if (!('includes' in String.prototype)) {
+ String.prototype.includes = function (searchElement) {
+ return this.indexOf(searchElement) > -1;
+ };
+}
+if (!('startsWith' in String.prototype)) {
+ String.prototype.startsWith = function (searchElement) {
+ return this.indexOf(searchElement) === 0;
+ };
+}
+if (!('endsWith' in String.prototype)) {
+ String.prototype.endsWith = function (searchElement) {
+ var i = this.indexOf(searchElement);
+ return i > -1 && i === this.length - searchElement.length;
+ };
+}
diff --git a/yarn.lock b/yarn.lock
index 0407a15e420c..7f4e13e3108b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2248,6 +2248,15 @@
resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7"
integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==
+"@dabh/diagnostics@^2.0.2":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a"
+ integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==
+ dependencies:
+ colorspace "1.1.x"
+ enabled "2.0.x"
+ kuler "^2.0.0"
+
"@discoveryjs/json-ext@0.5.3":
version "0.5.3"
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz#90420f9f9c6d3987f176a19a7d8e764271a2f55d"
@@ -5098,6 +5107,11 @@
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d"
integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==
+"@types/triple-beam@^1.3.2":
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.2.tgz#38ecb64f01aa0d02b7c8f4222d7c38af6316fef8"
+ integrity sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==
+
"@types/uglify-js@*":
version "3.13.1"
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.1.tgz#5e889e9e81e94245c75b6450600e1c5ea2878aea"
@@ -5832,6 +5846,11 @@ acorn-walk@^8.0.0, acorn-walk@^8.1.1, acorn-walk@^8.2.0:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
+acorn@8.8.2, acorn@^8.8.1, acorn@^8.8.2:
+ version "8.8.2"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
+ integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
+
acorn@^6.0.5, acorn@^6.4.1:
version "6.4.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
@@ -5847,11 +5866,6 @@ acorn@^8.0.4, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0, acorn@^8.7
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
-acorn@^8.8.1, acorn@^8.8.2:
- version "8.8.2"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
- integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
-
add-stream@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa"
@@ -9257,7 +9271,7 @@ collection-visit@^1.0.0:
map-visit "^1.0.0"
object-visit "^1.0.0"
-color-convert@^1.9.0, color-convert@^1.9.1:
+color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@@ -9289,6 +9303,14 @@ color-string@^1.5.4:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
+color-string@^1.6.0:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
+ integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
+ dependencies:
+ color-name "^1.0.0"
+ simple-swizzle "^0.2.2"
+
color-support@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
@@ -9302,6 +9324,14 @@ color@^3.0.0:
color-convert "^1.9.1"
color-string "^1.5.4"
+color@^3.1.3:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164"
+ integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==
+ dependencies:
+ color-convert "^1.9.3"
+ color-string "^1.6.0"
+
colord@^2.9.1:
version "2.9.3"
resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43"
@@ -9322,6 +9352,14 @@ colors@1.4.0, colors@^1.4.0:
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
+colorspace@1.1.x:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243"
+ integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==
+ dependencies:
+ color "^3.1.3"
+ text-hex "1.0.x"
+
columnify@1.6.0:
version "1.6.0"
resolved "https://registry.npmjs.org/columnify/-/columnify-1.6.0.tgz#6989531713c9008bb29735e61e37acf5bd553cf3"
@@ -9347,6 +9385,11 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
+commander@10.0.0:
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.0.tgz#71797971162cd3cf65f0b9d24eb28f8d303acdf1"
+ integrity sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==
+
commander@2.8.x:
version "2.8.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
@@ -11836,6 +11879,11 @@ emojis-list@^3.0.0:
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
+enabled@2.0.x:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
+ integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==
+
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
@@ -12020,6 +12068,17 @@ es-abstract@^1.17.2, es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-
string.prototype.trimstart "^1.0.5"
unbox-primitive "^1.0.2"
+es-check@7.1.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/es-check/-/es-check-7.1.0.tgz#1015db640a7b785ff4098baf2e0791c070a25964"
+ integrity sha512-t099vm9tNqNHF28Q/mRcqYxmkbkoo/Qu2ZI5/D+eFeqNUjI3jwkIyHyexXiAtstbZ1FQELi0QCuUaYCtiffi4Q==
+ dependencies:
+ acorn "8.8.2"
+ commander "10.0.0"
+ fast-glob "^3.2.12"
+ supports-color "^8.1.1"
+ winston "^3.8.2"
+
es-module-lexer@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.7.1.tgz#c2c8e0f46f2df06274cdaf0dd3f3b33e0a0b267d"
@@ -12905,7 +12964,7 @@ fast-glob@3.2.7:
merge2 "^1.3.0"
micromatch "^4.0.4"
-fast-glob@^3.0.3, fast-glob@^3.2.11, fast-glob@^3.2.4, fast-glob@^3.2.5, fast-glob@^3.2.9:
+fast-glob@^3.0.3, fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.4, fast-glob@^3.2.5, fast-glob@^3.2.9:
version "3.2.12"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
@@ -13000,6 +13059,11 @@ fd-slicer@~1.1.0:
dependencies:
pend "~1.2.0"
+fecha@^4.2.0:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd"
+ integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==
+
fflate@^0.4.4:
version "0.4.8"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
@@ -13341,6 +13405,11 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
+fn.name@1.x.x:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
+ integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
+
follow-redirects@^1.0.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
@@ -16988,6 +17057,11 @@ klona@^2.0.4:
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22"
integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==
+kuler@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
+ integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
+
language-subtag-registry@~0.3.2:
version "0.3.22"
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d"
@@ -17684,6 +17758,18 @@ log4js@^6.4.1:
rfdc "^1.3.0"
streamroller "^3.0.2"
+logform@^2.3.2, logform@^2.4.0:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/logform/-/logform-2.5.1.tgz#44c77c34becd71b3a42a3970c77929e52c6ed48b"
+ integrity sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==
+ dependencies:
+ "@colors/colors" "1.5.0"
+ "@types/triple-beam" "^1.3.2"
+ fecha "^4.2.0"
+ ms "^2.1.1"
+ safe-stable-stringify "^2.3.1"
+ triple-beam "^1.3.0"
+
loglevel@^1.6.8:
version "1.8.0"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114"
@@ -19850,6 +19936,13 @@ once@1.x, once@^1.3.0, once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
+one-time@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45"
+ integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==
+ dependencies:
+ fn.name "1.x.x"
+
onetime@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
@@ -24669,6 +24762,11 @@ stable@^0.1.8:
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+stack-trace@0.0.x:
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
+ integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==
+
stack-utils@^2.0.3:
version "2.0.5"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5"
@@ -25235,7 +25333,7 @@ supports-color@^7.0.0, supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
-supports-color@^8.0.0:
+supports-color@^8.0.0, supports-color@^8.1.1:
version "8.1.1"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
@@ -25644,6 +25742,11 @@ text-extensions@^1.0.0:
resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26"
integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==
+text-hex@1.0.x:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
+ integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
+
text-table@0.2.0, text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@@ -25985,6 +26088,11 @@ trim-right@^1.0.1:
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
+triple-beam@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
+ integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
+
ts-interface-checker@^0.1.9:
version "0.1.13"
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
@@ -27558,6 +27666,32 @@ wildcard@^2.0.0:
resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
+winston-transport@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa"
+ integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==
+ dependencies:
+ logform "^2.3.2"
+ readable-stream "^3.6.0"
+ triple-beam "^1.3.0"
+
+winston@^3.8.2:
+ version "3.8.2"
+ resolved "https://registry.yarnpkg.com/winston/-/winston-3.8.2.tgz#56e16b34022eb4cff2638196d9646d7430fdad50"
+ integrity sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==
+ dependencies:
+ "@colors/colors" "1.5.0"
+ "@dabh/diagnostics" "^2.0.2"
+ async "^3.2.3"
+ is-stream "^2.0.0"
+ logform "^2.4.0"
+ one-time "^1.0.0"
+ readable-stream "^3.4.0"
+ safe-stable-stringify "^2.3.1"
+ stack-trace "0.0.x"
+ triple-beam "^1.3.0"
+ winston-transport "^4.5.0"
+
word-wrap@^1.2.3, word-wrap@~1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
From ff469ab632707e2146958ca01bf6db45326159b2 Mon Sep 17 00:00:00 2001
From: Tim Fish
Date: Wed, 22 Mar 2023 16:41:18 +0000
Subject: [PATCH 22/34] feat(tracing): Remove some more `@sentry/tracing`
references (#7570)
---
packages/nextjs/test/clientSdk.test.ts | 5 +----
packages/nextjs/test/config/withSentry.test.ts | 5 +++++
packages/nextjs/test/edge/edgeWrapperUtils.test.ts | 5 +++++
packages/node/package.json | 1 -
packages/node/src/integrations/onuncaughtexception.ts | 1 -
packages/node/test/integrations/http.test.ts | 11 +++++------
6 files changed, 16 insertions(+), 12 deletions(-)
diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts
index 5bfb29434523..ed3bb666d58d 100644
--- a/packages/nextjs/test/clientSdk.test.ts
+++ b/packages/nextjs/test/clientSdk.test.ts
@@ -1,7 +1,6 @@
import { BaseClient, getCurrentHub } from '@sentry/core';
import * as SentryReact from '@sentry/react';
-import { WINDOW } from '@sentry/react';
-import { Integrations as TracingIntegrations } from '@sentry/tracing';
+import { BrowserTracing, WINDOW } from '@sentry/react';
import type { Integration } from '@sentry/types';
import type { UserIntegrationsFunction } from '@sentry/utils';
import { logger } from '@sentry/utils';
@@ -9,8 +8,6 @@ import { JSDOM } from 'jsdom';
import { init, Integrations, nextRouterInstrumentation } from '../src/client';
-const { BrowserTracing } = TracingIntegrations;
-
const reactInit = jest.spyOn(SentryReact, 'init');
const captureEvent = jest.spyOn(BaseClient.prototype, 'captureEvent');
const loggerLogSpy = jest.spyOn(logger, 'log');
diff --git a/packages/nextjs/test/config/withSentry.test.ts b/packages/nextjs/test/config/withSentry.test.ts
index dfc06c9bcf7e..92315374836b 100644
--- a/packages/nextjs/test/config/withSentry.test.ts
+++ b/packages/nextjs/test/config/withSentry.test.ts
@@ -1,4 +1,5 @@
import * as hub from '@sentry/core';
+import { addTracingExtensions } from '@sentry/core';
import * as Sentry from '@sentry/node';
import type { Client, ClientOptions } from '@sentry/types';
import type { NextApiRequest, NextApiResponse } from 'next';
@@ -6,6 +7,10 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { withSentry } from '../../src/server';
import type { AugmentedNextApiResponse, NextApiHandler } from '../../src/server/types';
+// The wrap* functions require the hub to have tracing extensions. This is normally called by the NodeClient
+// constructor but the client isn't used in these tests.
+addTracingExtensions();
+
const FLUSH_DURATION = 200;
async function sleep(ms: number): Promise {
diff --git a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts
index 852ceb5628b4..cdc7cc4986e2 100644
--- a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts
+++ b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts
@@ -1,7 +1,12 @@
import * as coreSdk from '@sentry/core';
+import { addTracingExtensions } from '@sentry/core';
import { withEdgeWrapping } from '../../src/edge/utils/edgeWrapperUtils';
+// The wrap* functions require the hub to have tracing extensions. This is normally called by the EdgeClient
+// constructor but the client isn't used in these tests.
+addTracingExtensions();
+
// @ts-ignore Request does not exist on type Global
const origRequest = global.Request;
// @ts-ignore Response does not exist on type Global
diff --git a/packages/node/package.json b/packages/node/package.json
index e29e7290c495..fd197f539739 100644
--- a/packages/node/package.json
+++ b/packages/node/package.json
@@ -26,7 +26,6 @@
"tslib": "^1.9.3"
},
"devDependencies": {
- "@sentry/tracing": "7.44.2",
"@types/cookie": "0.3.2",
"@types/express": "^4.17.14",
"@types/lru-cache": "^5.1.0",
diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts
index e55de4d1fd8e..b4f99b419fd4 100644
--- a/packages/node/src/integrations/onuncaughtexception.ts
+++ b/packages/node/src/integrations/onuncaughtexception.ts
@@ -105,7 +105,6 @@ export class OnUncaughtException implements Integration {
if (
// There are 3 listeners we ignore:
listener.name === 'domainUncaughtExceptionClear' || // as soon as we're using domains this listener is attached by node itself
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(listener.tag && listener.tag === 'sentry_tracingErrorCallback') || // the handler we register for tracing
listener === this.handler // the handler we register in this integration
) {
diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts
index 446aa4ec0f82..7d9a4e45226b 100644
--- a/packages/node/test/integrations/http.test.ts
+++ b/packages/node/test/integrations/http.test.ts
@@ -1,9 +1,8 @@
+import type { Span, Transaction } from '@sentry/core';
import * as sentryCore from '@sentry/core';
-import { Hub } from '@sentry/core';
-import type { Span, Transaction } from '@sentry/tracing';
-import { addExtensionMethods, TRACEPARENT_REGEXP } from '@sentry/tracing';
+import { addTracingExtensions, Hub } from '@sentry/core';
import type { TransactionContext } from '@sentry/types';
-import { logger, parseSemver } from '@sentry/utils';
+import { logger, parseSemver, TRACEPARENT_REGEXP } from '@sentry/utils';
import * as http from 'http';
import * as https from 'https';
import * as HttpsProxyAgent from 'https-proxy-agent';
@@ -34,7 +33,7 @@ describe('tracing', () => {
...customOptions,
});
const hub = new Hub(new NodeClient(options));
- addExtensionMethods();
+ addTracingExtensions();
hub.configureScope(scope =>
scope.setUser({
@@ -227,7 +226,7 @@ describe('tracing', () => {
}
function createTransactionAndPutOnScope(hub: Hub) {
- addExtensionMethods();
+ addTracingExtensions();
const transaction = hub.startTransaction({ name: 'dogpark' });
hub.getScope()?.setSpan(transaction);
return transaction;
From eb1a87cef3ac24c0d0aaa40a5853aae2110be875 Mon Sep 17 00:00:00 2001
From: Tim Fish
Date: Wed, 22 Mar 2023 16:42:15 +0000
Subject: [PATCH 23/34] feat(otel): Remove `@sentry/tracing` dependency from
`opentelemetry-node` (#7572)
---
packages/opentelemetry-node/package.json | 1 -
packages/opentelemetry-node/src/index.ts | 2 --
packages/opentelemetry-node/src/spanprocessor.ts | 4 +++-
packages/opentelemetry-node/test/propagator.test.ts | 5 ++---
packages/opentelemetry-node/test/spanprocessor.test.ts | 7 +++----
5 files changed, 8 insertions(+), 11 deletions(-)
diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json
index 84391b502c63..b19a608ef2e3 100644
--- a/packages/opentelemetry-node/package.json
+++ b/packages/opentelemetry-node/package.json
@@ -17,7 +17,6 @@
},
"dependencies": {
"@sentry/core": "7.44.2",
- "@sentry/tracing": "7.44.2",
"@sentry/types": "7.44.2",
"@sentry/utils": "7.44.2"
},
diff --git a/packages/opentelemetry-node/src/index.ts b/packages/opentelemetry-node/src/index.ts
index 501c93432b44..752c2cfaa0aa 100644
--- a/packages/opentelemetry-node/src/index.ts
+++ b/packages/opentelemetry-node/src/index.ts
@@ -1,4 +1,2 @@
-import '@sentry/tracing';
-
export { SentrySpanProcessor } from './spanprocessor';
export { SentryPropagator } from './propagator';
diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts
index c2d141e05f56..773c68ebf1ff 100644
--- a/packages/opentelemetry-node/src/spanprocessor.ts
+++ b/packages/opentelemetry-node/src/spanprocessor.ts
@@ -2,7 +2,7 @@ import type { Context } from '@opentelemetry/api';
import { SpanKind, trace } from '@opentelemetry/api';
import type { Span as OtelSpan, SpanProcessor as OtelSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
-import { addGlobalEventProcessor, getCurrentHub, Transaction } from '@sentry/core';
+import { addGlobalEventProcessor, addTracingExtensions, getCurrentHub, Transaction } from '@sentry/core';
import type { DynamicSamplingContext, Span as SentrySpan, TraceparentData, TransactionContext } from '@sentry/types';
import { isString, logger } from '@sentry/utils';
@@ -22,6 +22,8 @@ export const SENTRY_SPAN_PROCESSOR_MAP: Map =
*/
export class SentrySpanProcessor implements OtelSpanProcessor {
public constructor() {
+ addTracingExtensions();
+
addGlobalEventProcessor(event => {
const otelSpan = trace && trace.getActiveSpan && (trace.getActiveSpan() as OtelSpan | undefined);
if (!otelSpan) {
diff --git a/packages/opentelemetry-node/test/propagator.test.ts b/packages/opentelemetry-node/test/propagator.test.ts
index f1914653dd0d..d5222e3103d4 100644
--- a/packages/opentelemetry-node/test/propagator.test.ts
+++ b/packages/opentelemetry-node/test/propagator.test.ts
@@ -7,8 +7,7 @@ import {
TraceFlags,
} from '@opentelemetry/api';
import { suppressTracing } from '@opentelemetry/core';
-import { Hub, makeMain } from '@sentry/core';
-import { addExtensionMethods, Transaction } from '@sentry/tracing';
+import { addTracingExtensions, Hub, makeMain, Transaction } from '@sentry/core';
import type { TransactionContext } from '@sentry/types';
import {
@@ -21,7 +20,7 @@ import { SentryPropagator } from '../src/propagator';
import { SENTRY_SPAN_PROCESSOR_MAP } from '../src/spanprocessor';
beforeAll(() => {
- addExtensionMethods();
+ addTracingExtensions();
});
describe('SentryPropagator', () => {
diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts
index 4e3c975d00cf..94abd327314c 100644
--- a/packages/opentelemetry-node/test/spanprocessor.test.ts
+++ b/packages/opentelemetry-node/test/spanprocessor.test.ts
@@ -4,10 +4,9 @@ import { Resource } from '@opentelemetry/resources';
import type { Span as OtelSpan } from '@opentelemetry/sdk-trace-base';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { SemanticAttributes, SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
-import { createTransport, Hub, makeMain } from '@sentry/core';
+import type { SpanStatusType } from '@sentry/core';
+import { addTracingExtensions, createTransport, Hub, makeMain, Span as SentrySpan, Transaction } from '@sentry/core';
import { NodeClient } from '@sentry/node';
-import type { SpanStatusType } from '@sentry/tracing';
-import { addExtensionMethods, Span as SentrySpan, Transaction } from '@sentry/tracing';
import { resolvedSyncPromise } from '@sentry/utils';
import { SENTRY_SPAN_PROCESSOR_MAP, SentrySpanProcessor } from '../src/spanprocessor';
@@ -24,7 +23,7 @@ const DEFAULT_NODE_CLIENT_OPTIONS = {
// Integration Test of SentrySpanProcessor
beforeAll(() => {
- addExtensionMethods();
+ addTracingExtensions();
});
describe('SentrySpanProcessor', () => {
From 98b6a1c1f3504d20594c190450cd13d36f6968b4 Mon Sep 17 00:00:00 2001
From: Tim Fish
Date: Wed, 22 Mar 2023 18:25:02 +0000
Subject: [PATCH 24/34] feat(remix): Remove `@sentry/tracing` dependency from
Remix SDK (#7575)
---
packages/remix/package.json | 1 -
packages/remix/src/index.client.tsx | 5 +----
packages/remix/src/index.server.ts | 1 -
packages/remix/test/index.server.test.ts | 7 ++++++-
4 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/packages/remix/package.json b/packages/remix/package.json
index d08489a442c9..a2d68a4e0c38 100644
--- a/packages/remix/package.json
+++ b/packages/remix/package.json
@@ -25,7 +25,6 @@
"@sentry/integrations": "7.44.2",
"@sentry/node": "7.44.2",
"@sentry/react": "7.44.2",
- "@sentry/tracing": "7.44.2",
"@sentry/types": "7.44.2",
"@sentry/utils": "7.44.2",
"@sentry/webpack-plugin": "1.19.0",
diff --git a/packages/remix/src/index.client.tsx b/packages/remix/src/index.client.tsx
index 22d9ce73fe82..5c76ee4907bf 100644
--- a/packages/remix/src/index.client.tsx
+++ b/packages/remix/src/index.client.tsx
@@ -1,14 +1,11 @@
/* eslint-disable import/export */
-import { configureScope, init as reactInit, Integrations } from '@sentry/react';
+import { configureScope, init as reactInit } from '@sentry/react';
import { buildMetadata } from './utils/metadata';
import type { RemixOptions } from './utils/remixOptions';
export { remixRouterInstrumentation, withSentry } from './performance/client';
-export { BrowserTracing } from '@sentry/tracing';
export * from '@sentry/react';
-export { Integrations };
-
export function init(options: RemixOptions): void {
buildMetadata(options, ['remix', 'react']);
options.environment = options.environment || process.env.NODE_ENV;
diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts
index b2ad73866fde..21cfc20b17ab 100644
--- a/packages/remix/src/index.server.ts
+++ b/packages/remix/src/index.server.ts
@@ -8,7 +8,6 @@ import type { RemixOptions } from './utils/remixOptions';
export { ErrorBoundary, withErrorBoundary } from '@sentry/react';
export { remixRouterInstrumentation, withSentry } from './performance/client';
-export { BrowserTracing, Integrations } from '@sentry/tracing';
export * from '@sentry/node';
export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express';
diff --git a/packages/remix/test/index.server.test.ts b/packages/remix/test/index.server.test.ts
index f4d4f9c95fe0..ec1610aee400 100644
--- a/packages/remix/test/index.server.test.ts
+++ b/packages/remix/test/index.server.test.ts
@@ -2,7 +2,7 @@ import * as SentryNode from '@sentry/node';
import { getCurrentHub } from '@sentry/node';
import { GLOBAL_OBJ } from '@sentry/utils';
-import { init } from '../src/index.server';
+import { init, Integrations } from '../src/index.server';
const nodeInit = jest.spyOn(SentryNode, 'init');
@@ -57,4 +57,9 @@ describe('Server init()', () => {
// @ts-ignore need access to protected _tags attribute
expect(currentScope._tags).toEqual({ runtime: 'node' });
});
+
+ it('has both node and tracing integrations', () => {
+ expect(Integrations.Apollo).not.toBeUndefined();
+ expect(Integrations.Http).not.toBeUndefined();
+ });
});
From f38ad39477fb8d9bdfa6676dc1851d4cc31d46ca Mon Sep 17 00:00:00 2001
From: Tim Fish
Date: Wed, 22 Mar 2023 21:13:19 +0000
Subject: [PATCH 25/34] feat(gatsby): Remove `@sentry/tracing` dependency from
Gatsby SDK (#7578)
---
packages/gatsby/package.json | 1 -
packages/gatsby/src/index.ts | 1 -
packages/gatsby/src/utils/integrations.ts | 9 +--
packages/gatsby/test/gatsby-browser.test.ts | 10 +--
packages/gatsby/test/sdk.test.ts | 74 ++++++---------------
packages/node/test/handlers.test.ts | 2 +-
6 files changed, 30 insertions(+), 67 deletions(-)
diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json
index 1016f7cfa754..732c0836a08f 100644
--- a/packages/gatsby/package.json
+++ b/packages/gatsby/package.json
@@ -22,7 +22,6 @@
"dependencies": {
"@sentry/core": "7.44.2",
"@sentry/react": "7.44.2",
- "@sentry/tracing": "7.44.2",
"@sentry/types": "7.44.2",
"@sentry/utils": "7.44.2",
"@sentry/webpack-plugin": "1.19.0"
diff --git a/packages/gatsby/src/index.ts b/packages/gatsby/src/index.ts
index 6957f7337700..7c603d040693 100644
--- a/packages/gatsby/src/index.ts
+++ b/packages/gatsby/src/index.ts
@@ -1,4 +1,3 @@
export * from '@sentry/react';
-export { Integrations } from '@sentry/tracing';
export { init } from './sdk';
diff --git a/packages/gatsby/src/utils/integrations.ts b/packages/gatsby/src/utils/integrations.ts
index 680ef61765cc..94ef28f21272 100644
--- a/packages/gatsby/src/utils/integrations.ts
+++ b/packages/gatsby/src/utils/integrations.ts
@@ -1,5 +1,5 @@
import { hasTracingEnabled } from '@sentry/core';
-import * as Tracing from '@sentry/tracing';
+import { BrowserTracing } from '@sentry/react';
import type { Integration } from '@sentry/types';
import type { GatsbyOptions } from './types';
@@ -31,11 +31,8 @@ export function getIntegrationsFromOptions(options: GatsbyOptions): UserIntegrat
* @param isTracingEnabled Whether the user has enabled tracing.
*/
function getIntegrationsFromArray(userIntegrations: Integration[], isTracingEnabled: boolean): Integration[] {
- if (
- isTracingEnabled &&
- !userIntegrations.some(integration => integration.name === Tracing.Integrations.BrowserTracing.name)
- ) {
- userIntegrations.push(new Tracing.Integrations.BrowserTracing());
+ if (isTracingEnabled && !userIntegrations.some(integration => integration.name === BrowserTracing.name)) {
+ userIntegrations.push(new BrowserTracing());
}
return userIntegrations;
}
diff --git a/packages/gatsby/test/gatsby-browser.test.ts b/packages/gatsby/test/gatsby-browser.test.ts
index 00be51488d5f..b67305042c71 100644
--- a/packages/gatsby/test/gatsby-browser.test.ts
+++ b/packages/gatsby/test/gatsby-browser.test.ts
@@ -2,6 +2,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { onClientEntry } from '../gatsby-browser';
+import { BrowserTracing } from '../src/index';
(global as any).__SENTRY_RELEASE__ = '683f3a6ab819d47d23abfca9a914c81f0524d35b';
(global as any).__SENTRY_DSN__ = 'https://examplePublicKey@o0.ingest.sentry.io/0';
@@ -20,11 +21,11 @@ global.console.warn = jest.fn();
global.console.error = jest.fn();
let tracingAddExtensionMethods = jest.fn();
-jest.mock('@sentry/tracing', () => {
- const original = jest.requireActual('@sentry/tracing');
+jest.mock('@sentry/core', () => {
+ const original = jest.requireActual('@sentry/core');
return {
...original,
- addExtensionMethods: (...args: any[]) => {
+ addTracingExtensions: (...args: any[]) => {
tracingAddExtensionMethods(...args);
},
};
@@ -140,8 +141,7 @@ describe('onClientEntry', () => {
});
it('only defines a single `BrowserTracing` integration', () => {
- const Tracing = jest.requireActual('@sentry/tracing');
- const integrations = [new Tracing.Integrations.BrowserTracing()];
+ const integrations = [new BrowserTracing()];
onClientEntry(undefined, { tracesSampleRate: 0.5, integrations });
expect(sentryInit).toHaveBeenLastCalledWith(
diff --git a/packages/gatsby/test/sdk.test.ts b/packages/gatsby/test/sdk.test.ts
index 1c4342a13a4b..082fd771060b 100644
--- a/packages/gatsby/test/sdk.test.ts
+++ b/packages/gatsby/test/sdk.test.ts
@@ -1,5 +1,4 @@
-import { init, SDK_VERSION } from '@sentry/react';
-import { Integrations } from '@sentry/tracing';
+import { BrowserTracing, init, SDK_VERSION } from '@sentry/react';
import type { Integration } from '@sentry/types';
import { init as gatsbyInit } from '../src/sdk';
@@ -58,6 +57,8 @@ describe('Initialize React SDK', () => {
});
});
+type TestArgs = [string, Integration[], GatsbyOptions, string[]];
+
describe('Integrations from options', () => {
afterEach(() => reactInit.mockClear());
@@ -65,72 +66,39 @@ describe('Integrations from options', () => {
['tracing disabled, no integrations', [], {}, []],
['tracing enabled, no integrations', [], { tracesSampleRate: 1 }, ['BrowserTracing']],
[
- 'tracing disabled, with Integrations.BrowserTracing as an array',
+ 'tracing disabled, with BrowserTracing as an array',
[],
- { integrations: [new Integrations.BrowserTracing()] },
+ { integrations: [new BrowserTracing()] },
['BrowserTracing'],
],
[
- 'tracing disabled, with Integrations.BrowserTracing as a function',
+ 'tracing disabled, with BrowserTracing as a function',
[],
{
- integrations: () => [new Integrations.BrowserTracing()],
+ integrations: () => [new BrowserTracing()],
},
['BrowserTracing'],
],
[
- 'tracing enabled, with Integrations.BrowserTracing as an array',
+ 'tracing enabled, with BrowserTracing as an array',
[],
- { tracesSampleRate: 1, integrations: [new Integrations.BrowserTracing()] },
+ { tracesSampleRate: 1, integrations: [new BrowserTracing()] },
['BrowserTracing'],
],
[
- 'tracing enabled, with Integrations.BrowserTracing as a function',
+ 'tracing enabled, with BrowserTracing as a function',
[],
- { tracesSampleRate: 1, integrations: () => [new Integrations.BrowserTracing()] },
+ { tracesSampleRate: 1, integrations: () => [new BrowserTracing()] },
['BrowserTracing'],
],
- [
- 'tracing enabled, with another integration as an array',
- [],
- { tracesSampleRate: 1, integrations: [new Integrations.Express()] },
- ['Express', 'BrowserTracing'],
- ],
- [
- 'tracing enabled, with another integration as a function',
- [],
- { tracesSampleRate: 1, integrations: () => [new Integrations.Express()] },
- ['Express', 'BrowserTracing'],
- ],
- [
- 'tracing disabled, with another integration as an array',
- [],
- { integrations: [new Integrations.Express()] },
- ['Express'],
- ],
- [
- 'tracing disabled, with another integration as a function',
- [],
- { integrations: () => [new Integrations.Express()] },
- ['Express'],
- ],
- [
- 'merges integrations with user integrations as a function',
- [new Integrations.Mongo()],
- {
- tracesSampleRate: 1,
- integrations: (defaultIntegrations: Integration[]): Integration[] => [
- ...defaultIntegrations,
- new Integrations.Express(),
- ],
- },
- ['Mongo', 'Express', 'BrowserTracing'],
- ],
- ])('%s', (_testName, defaultIntegrations: Integration[], options: GatsbyOptions, expectedIntNames: string[]) => {
- gatsbyInit(options);
- const integrations: UserIntegrations = reactInit.mock.calls[0][0].integrations;
- const arrIntegrations = Array.isArray(integrations) ? integrations : integrations(defaultIntegrations);
- expect(arrIntegrations).toHaveLength(expectedIntNames.length);
- arrIntegrations.map((integration, idx) => expect(integration.name).toStrictEqual(expectedIntNames[idx]));
- });
+ ] as TestArgs[])(
+ '%s',
+ (_testName, defaultIntegrations: Integration[], options: GatsbyOptions, expectedIntNames: string[]) => {
+ gatsbyInit(options);
+ const integrations: UserIntegrations = reactInit.mock.calls[0][0].integrations;
+ const arrIntegrations = Array.isArray(integrations) ? integrations : integrations(defaultIntegrations);
+ expect(arrIntegrations).toHaveLength(expectedIntNames.length);
+ arrIntegrations.map((integration, idx) => expect(integration.name).toStrictEqual(expectedIntNames[idx]));
+ },
+ );
});
diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts
index 03cfba80ade4..028cdff98af1 100644
--- a/packages/node/test/handlers.test.ts
+++ b/packages/node/test/handlers.test.ts
@@ -1,5 +1,5 @@
import * as sentryCore from '@sentry/core';
-import { Transaction } from '@sentry/tracing';
+import { Transaction } from '@sentry/core';
import type { Event } from '@sentry/types';
import { SentryError } from '@sentry/utils';
import * as http from 'http';
From 383e92927285e9ffe7305b2c0a4f12aa0c46795c Mon Sep 17 00:00:00 2001
From: Tim Fish
Date: Wed, 22 Mar 2023 21:14:06 +0000
Subject: [PATCH 26/34] feat(serverless): Remove `@sentry/tracing` dependency
from serverless SDK (#7579)
---
packages/serverless/package.json | 1 -
packages/serverless/scripts/buildLambdaLayer.ts | 2 +-
packages/serverless/src/index.ts | 1 +
3 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/serverless/package.json b/packages/serverless/package.json
index e68ec304dbe3..522d2419814c 100644
--- a/packages/serverless/package.json
+++ b/packages/serverless/package.json
@@ -17,7 +17,6 @@
},
"dependencies": {
"@sentry/node": "7.44.2",
- "@sentry/tracing": "7.44.2",
"@sentry/types": "7.44.2",
"@sentry/utils": "7.44.2",
"@types/aws-lambda": "^8.10.62",
diff --git a/packages/serverless/scripts/buildLambdaLayer.ts b/packages/serverless/scripts/buildLambdaLayer.ts
index c7e2199aedbb..459560a660fe 100644
--- a/packages/serverless/scripts/buildLambdaLayer.ts
+++ b/packages/serverless/scripts/buildLambdaLayer.ts
@@ -17,7 +17,7 @@ async function buildLambdaLayer(): Promise {
// Create the main SDK bundle
// TODO: Check if we can get rid of this, after the lerna 6/nx update??
await ensureBundleBuildPrereqs({
- dependencies: ['@sentry/utils', '@sentry/hub', '@sentry/core', '@sentry/tracing', '@sentry/node'],
+ dependencies: ['@sentry/utils', '@sentry/hub', '@sentry/core', '@sentry/node'],
});
run('yarn rollup --config rollup.aws.config.js');
diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts
index bd552060c6dd..e0513a53cd3e 100644
--- a/packages/serverless/src/index.ts
+++ b/packages/serverless/src/index.ts
@@ -20,6 +20,7 @@ export {
captureMessage,
configureScope,
createTransport,
+ getActiveTransaction,
getCurrentHub,
getHubFromCarrier,
makeMain,
From 551aedcc8eb6392f66af840c0b6a4c45032ba05e Mon Sep 17 00:00:00 2001
From: Francesco Novy
Date: Thu, 23 Mar 2023 08:40:15 +0100
Subject: [PATCH 27/34] test(replay): Add replay E2E test (#7486)
---------
Co-authored-by: Lukas Stracke
---
.../standard-frontend-react/src/globals.d.ts | 1 +
.../standard-frontend-react/src/index.tsx | 13 +
.../tests/behaviour-test.spec.ts | 83 +++++++
.../tests/fixtures/ReplayRecordingData.ts | 225 ++++++++++++++++++
4 files changed, 322 insertions(+)
create mode 100644 packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts
diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/src/globals.d.ts b/packages/e2e-tests/test-applications/standard-frontend-react/src/globals.d.ts
index 109dbcd55648..ffa61ca49acc 100644
--- a/packages/e2e-tests/test-applications/standard-frontend-react/src/globals.d.ts
+++ b/packages/e2e-tests/test-applications/standard-frontend-react/src/globals.d.ts
@@ -1,4 +1,5 @@
interface Window {
recordedTransactions?: string[];
capturedExceptionId?: string;
+ sentryReplayId?: string;
}
diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/src/index.tsx b/packages/e2e-tests/test-applications/standard-frontend-react/src/index.tsx
index c6b8db266ac0..ef820ec794b3 100644
--- a/packages/e2e-tests/test-applications/standard-frontend-react/src/index.tsx
+++ b/packages/e2e-tests/test-applications/standard-frontend-react/src/index.tsx
@@ -14,6 +14,8 @@ import {
import Index from './pages/Index';
import User from './pages/User';
+const replay = new Sentry.Replay();
+
Sentry.init({
dsn: process.env.REACT_APP_E2E_TEST_DSN,
integrations: [
@@ -26,11 +28,22 @@ Sentry.init({
matchRoutes,
),
}),
+ replay,
],
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0,
release: 'e2e-test',
+
+ // Always capture replays, so we can test this properly
+ replaysSessionSampleRate: 1.0,
+ replaysOnErrorSampleRate: 0.0,
+});
+
+Object.defineProperty(window, 'sentryReplayId', {
+ get() {
+ return replay['_replay'].session.id;
+ },
});
Sentry.addGlobalEventProcessor(event => {
diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/tests/behaviour-test.spec.ts b/packages/e2e-tests/test-applications/standard-frontend-react/tests/behaviour-test.spec.ts
index 795d610a4b08..fb2d291dd70d 100644
--- a/packages/e2e-tests/test-applications/standard-frontend-react/tests/behaviour-test.spec.ts
+++ b/packages/e2e-tests/test-applications/standard-frontend-react/tests/behaviour-test.spec.ts
@@ -1,5 +1,6 @@
import { test, expect } from '@playwright/test';
import axios, { AxiosError } from 'axios';
+import { ReplayRecordingData } from './fixtures/ReplayRecordingData';
const EVENT_POLLING_TIMEOUT = 30_000;
@@ -169,3 +170,85 @@ test('Sends a navigation transaction to Sentry', async ({ page }) => {
expect(hadPageNavigationTransaction).toBe(true);
});
+
+test('Sends a Replay recording to Sentry', async ({ browser }) => {
+ const context = await browser.newContext();
+ const page = await context.newPage();
+
+ await page.goto('/');
+
+ const replayId = await page.waitForFunction(() => {
+ return window.sentryReplayId;
+ });
+
+ // Wait for replay to be sent
+
+ if (replayId === undefined) {
+ throw new Error("Application didn't set a replayId");
+ }
+
+ console.log(`Polling for replay with ID: ${replayId}`);
+
+ await expect
+ .poll(
+ async () => {
+ try {
+ const response = await axios.get(
+ `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/replays/${replayId}/`,
+ { headers: { Authorization: `Bearer ${authToken}` } },
+ );
+
+ return response.status;
+ } catch (e) {
+ if (e instanceof AxiosError && e.response) {
+ if (e.response.status !== 404) {
+ throw e;
+ } else {
+ return e.response.status;
+ }
+ } else {
+ throw e;
+ }
+ }
+ },
+ {
+ timeout: EVENT_POLLING_TIMEOUT,
+ },
+ )
+ .toBe(200);
+
+ // now fetch the first recording segment
+ await expect
+ .poll(
+ async () => {
+ try {
+ const response = await axios.get(
+ `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/replays/${replayId}/recording-segments/?cursor=100%3A0%3A1`,
+ { headers: { Authorization: `Bearer ${authToken}` } },
+ );
+
+ return {
+ status: response.status,
+ data: response.data,
+ };
+ } catch (e) {
+ if (e instanceof AxiosError && e.response) {
+ if (e.response.status !== 404) {
+ throw e;
+ } else {
+ return e.response.status;
+ }
+ } else {
+ throw e;
+ }
+ }
+ },
+ {
+ timeout: EVENT_POLLING_TIMEOUT,
+ },
+ )
+ .toEqual({
+ status: 200,
+ data: ReplayRecordingData,
+ });
+});
diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts b/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts
new file mode 100644
index 000000000000..318fc368f7b9
--- /dev/null
+++ b/packages/e2e-tests/test-applications/standard-frontend-react/tests/fixtures/ReplayRecordingData.ts
@@ -0,0 +1,225 @@
+import { expect } from '@playwright/test';
+
+export const ReplayRecordingData = [
+ [
+ { type: 4, data: { href: 'http://localhost:3000/', width: 1280, height: 720 }, timestamp: expect.any(Number) },
+ {
+ type: 2,
+ data: {
+ node: {
+ type: 0,
+ childNodes: [
+ { type: 1, name: 'html', publicId: '', systemId: '', id: 2 },
+ {
+ type: 2,
+ tagName: 'html',
+ attributes: { lang: 'en' },
+ childNodes: [
+ {
+ type: 2,
+ tagName: 'head',
+ attributes: {},
+ childNodes: [
+ { type: 2, tagName: 'meta', attributes: { charset: 'utf-8' }, childNodes: [], id: 5 },
+ {
+ type: 2,
+ tagName: 'meta',
+ attributes: { name: 'viewport', content: 'width=device-width,initial-scale=1' },
+ childNodes: [],
+ id: 6,
+ },
+ {
+ type: 2,
+ tagName: 'meta',
+ attributes: { name: 'theme-color', content: '#000000' },
+ childNodes: [],
+ id: 7,
+ },
+ {
+ type: 2,
+ tagName: 'title',
+ attributes: {},
+ childNodes: [{ type: 3, textContent: '***** ***', id: 9 }],
+ id: 8,
+ },
+ ],
+ id: 4,
+ },
+ {
+ type: 2,
+ tagName: 'body',
+ attributes: {},
+ childNodes: [
+ {
+ type: 2,
+ tagName: 'noscript',
+ attributes: {},
+ childNodes: [{ type: 3, textContent: '*** **** ** ****** ********** ** *** **** ****', id: 12 }],
+ id: 11,
+ },
+ { type: 2, tagName: 'div', attributes: { id: 'root' }, childNodes: [], id: 13 },
+ ],
+ id: 10,
+ },
+ ],
+ id: 3,
+ },
+ ],
+ id: 1,
+ },
+ initialOffset: { left: 0, top: 0 },
+ },
+ timestamp: expect.any(Number),
+ },
+ {
+ type: 5,
+ timestamp: expect.any(Number),
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ op: 'memory',
+ description: 'memory',
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ data: {
+ memory: {
+ jsHeapSizeLimit: expect.any(Number),
+ totalJSHeapSize: expect.any(Number),
+ usedJSHeapSize: expect.any(Number),
+ },
+ },
+ },
+ },
+ },
+ {
+ type: 3,
+ data: {
+ source: 0,
+ texts: [],
+ attributes: [],
+ removes: [],
+ adds: [
+ {
+ parentId: 13,
+ nextId: null,
+ node: {
+ type: 2,
+ tagName: 'a',
+ attributes: { id: 'navigation', href: 'http://localhost:3000/user/5' },
+ childNodes: [],
+ id: 14,
+ },
+ },
+ { parentId: 14, nextId: null, node: { type: 3, textContent: '********', id: 15 } },
+ {
+ parentId: 13,
+ nextId: 14,
+ node: {
+ type: 2,
+ tagName: 'input',
+ attributes: { type: 'button', id: 'exception-button', value: '******* *********' },
+ childNodes: [],
+ id: 16,
+ },
+ },
+ ],
+ },
+ timestamp: expect.any(Number),
+ },
+ {
+ type: 3,
+ data: { source: 5, text: 'Capture Exception', isChecked: false, id: 16 },
+ timestamp: expect.any(Number),
+ },
+ ],
+ [
+ {
+ type: 5,
+ timestamp: expect.any(Number),
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ op: 'navigation.navigate',
+ description: 'http://localhost:3000/',
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ data: { size: expect.any(Number), duration: expect.any(Number) },
+ },
+ },
+ },
+ {
+ type: 5,
+ timestamp: expect.any(Number),
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ op: 'resource.script',
+ description: expect.stringMatching(/http:\/\/localhost:3000\/static\/js\/main.(\w+).js/),
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ data: { size: expect.any(Number), encodedBodySize: expect.any(Number) },
+ },
+ },
+ },
+ {
+ type: 5,
+ timestamp: expect.any(Number),
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ op: 'largest-contentful-paint',
+ description: 'largest-contentful-paint',
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ data: { value: expect.any(Number), size: expect.any(Number), nodeId: 16 },
+ },
+ },
+ },
+ {
+ type: 5,
+ timestamp: expect.any(Number),
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ op: 'paint',
+ description: 'first-paint',
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ },
+ },
+ },
+ {
+ type: 5,
+ timestamp: expect.any(Number),
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ op: 'paint',
+ description: 'first-contentful-paint',
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ },
+ },
+ },
+ {
+ type: 5,
+ timestamp: expect.any(Number),
+ data: {
+ tag: 'performanceSpan',
+ payload: {
+ op: 'memory',
+ description: 'memory',
+ startTimestamp: expect.any(Number),
+ endTimestamp: expect.any(Number),
+ data: {
+ memory: {
+ jsHeapSizeLimit: expect.any(Number),
+ totalJSHeapSize: expect.any(Number),
+ usedJSHeapSize: expect.any(Number),
+ },
+ },
+ },
+ },
+ },
+ ],
+];
From 0a1a567bc3ec18c9d044923134aa656fe9287de4 Mon Sep 17 00:00:00 2001
From: Francesco Novy
Date: Thu, 23 Mar 2023 09:15:35 +0100
Subject: [PATCH 28/34] fix(core): Ensure `ignoreErrors` only applies to error
events (#7573)
---
packages/core/src/integrations/inboundfilters.ts | 3 ++-
.../test/lib/integrations/inboundfilters.test.ts | 12 ++++++++++++
2 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts
index 047060ae4961..e790e3daf4b5 100644
--- a/packages/core/src/integrations/inboundfilters.ts
+++ b/packages/core/src/integrations/inboundfilters.ts
@@ -103,7 +103,8 @@ export function _shouldDropEvent(event: Event, options: Partial): boolean {
- if (!ignoreErrors || !ignoreErrors.length) {
+ // If event.type, this is not an error
+ if (event.type || !ignoreErrors || !ignoreErrors.length) {
return false;
}
diff --git a/packages/core/test/lib/integrations/inboundfilters.test.ts b/packages/core/test/lib/integrations/inboundfilters.test.ts
index ff9aca20270a..7537f19d4d5d 100644
--- a/packages/core/test/lib/integrations/inboundfilters.test.ts
+++ b/packages/core/test/lib/integrations/inboundfilters.test.ts
@@ -177,6 +177,11 @@ const MALFORMED_EVENT: Event = {
},
};
+const TRANSACTION_EVENT: Event = {
+ message: 'transaction message',
+ type: 'transaction',
+};
+
describe('InboundFilters', () => {
describe('_isSentryError', () => {
it('should work as expected', () => {
@@ -202,6 +207,13 @@ describe('InboundFilters', () => {
expect(eventProcessor(MESSAGE_EVENT, {})).toBe(null);
});
+ it('ignores transaction event for filtering', () => {
+ const eventProcessor = createInboundFiltersEventProcessor({
+ ignoreErrors: ['transaction'],
+ });
+ expect(eventProcessor(TRANSACTION_EVENT, {})).toBe(TRANSACTION_EVENT);
+ });
+
it('string filter with exact match', () => {
const eventProcessor = createInboundFiltersEventProcessor({
ignoreErrors: ['captureMessage'],
From 579d119f77d3e63f157d1de88049007d444f291e Mon Sep 17 00:00:00 2001
From: Francesco Novy
Date: Thu, 23 Mar 2023 10:14:57 +0100
Subject: [PATCH 29/34] feat(replay): Add `replay_id` to transaction DSC
(#7571)
---
.../suites/replay/dsc/init.js | 23 +++++++++++++
.../suites/replay/dsc/test.ts | 33 +++++++++++++++++++
.../utils/replayHelpers.ts | 10 ++++++
packages/core/src/baseclient.ts | 7 ++++
packages/core/src/tracing/transaction.ts | 2 ++
.../replay/src/util/addGlobalListeners.ts | 23 +++++++++----
packages/types/src/client.ts | 14 ++++++--
packages/types/src/envelope.ts | 1 +
8 files changed, 104 insertions(+), 9 deletions(-)
create mode 100644 packages/browser-integration-tests/suites/replay/dsc/init.js
create mode 100644 packages/browser-integration-tests/suites/replay/dsc/test.ts
diff --git a/packages/browser-integration-tests/suites/replay/dsc/init.js b/packages/browser-integration-tests/suites/replay/dsc/init.js
new file mode 100644
index 000000000000..c43f001779eb
--- /dev/null
+++ b/packages/browser-integration-tests/suites/replay/dsc/init.js
@@ -0,0 +1,23 @@
+import * as Sentry from '@sentry/browser';
+import { Integrations } from '@sentry/tracing';
+
+window.Sentry = Sentry;
+window.Replay = new Sentry.Replay({
+ flushMinDelay: 200,
+ flushMaxDelay: 200,
+ useCompression: false,
+});
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [new Integrations.BrowserTracing({ tracingOrigins: [/.*/] }), window.Replay],
+ environment: 'production',
+ tracesSampleRate: 1,
+ replaysSessionSampleRate: 0.0,
+ replaysOnErrorSampleRate: 1.0,
+});
+
+Sentry.configureScope(scope => {
+ scope.setUser({ id: 'user123', segment: 'segmentB' });
+ scope.setTransactionName('testTransactionDSC');
+});
diff --git a/packages/browser-integration-tests/suites/replay/dsc/test.ts b/packages/browser-integration-tests/suites/replay/dsc/test.ts
new file mode 100644
index 000000000000..0819e9f7bf71
--- /dev/null
+++ b/packages/browser-integration-tests/suites/replay/dsc/test.ts
@@ -0,0 +1,33 @@
+import { expect } from '@playwright/test';
+import type { EventEnvelopeHeaders } from '@sentry/types';
+
+import { sentryTest } from '../../../utils/fixtures';
+import { envelopeHeaderRequestParser, getFirstSentryEnvelopeRequest } from '../../../utils/helpers';
+import { getReplaySnapshot, shouldSkipReplayTest, waitForReplayRunning } from '../../../utils/replayHelpers';
+
+sentryTest('should add replay_id to dsc of transactions', async ({ getLocalTestPath, page, browserName }) => {
+ // This is flaky on webkit, so skipping there...
+ if (shouldSkipReplayTest() || browserName === 'webkit') {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+ await page.goto(url);
+
+ const envHeader = await getFirstSentryEnvelopeRequest(page, url, envelopeHeaderRequestParser);
+
+ await waitForReplayRunning(page);
+ const replay = await getReplaySnapshot(page);
+
+ expect(replay.session?.id).toBeDefined();
+
+ expect(envHeader.trace).toBeDefined();
+ expect(envHeader.trace).toEqual({
+ environment: 'production',
+ user_segment: 'segmentB',
+ sample_rate: '1',
+ trace_id: expect.any(String),
+ public_key: 'public',
+ replay_id: replay.session?.id,
+ });
+});
diff --git a/packages/browser-integration-tests/utils/replayHelpers.ts b/packages/browser-integration-tests/utils/replayHelpers.ts
index cf21ce7b9c7b..bd7696ebd927 100644
--- a/packages/browser-integration-tests/utils/replayHelpers.ts
+++ b/packages/browser-integration-tests/utils/replayHelpers.ts
@@ -99,6 +99,16 @@ function isCustomSnapshot(event: RecordingEvent): event is RecordingEvent & { da
return event.type === EventType.Custom;
}
+/** Wait for replay to be running & available. */
+export async function waitForReplayRunning(page: Page): Promise {
+ await page.waitForFunction(() => {
+ const replayIntegration = (window as unknown as Window & { Replay: { _replay: ReplayContainer } }).Replay;
+ const replay = replayIntegration._replay;
+
+ return replay.isEnabled() && replay.session?.id !== undefined;
+ });
+}
+
/**
* This returns the replay container (assuming it exists).
* Note that due to how this works with playwright, this is a POJO copy of replay.
diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts
index a6db2788aa52..80be9dc037bd 100644
--- a/packages/core/src/baseclient.ts
+++ b/packages/core/src/baseclient.ts
@@ -6,6 +6,7 @@ import type {
ClientOptions,
DataCategory,
DsnComponents,
+ DynamicSamplingContext,
Envelope,
ErrorEvent,
Event,
@@ -378,6 +379,9 @@ export abstract class BaseClient implements Client {
/** @inheritdoc */
public on(hook: 'beforeAddBreadcrumb', callback: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => void): void;
+ /** @inheritdoc */
+ public on(hook: 'createDsc', callback: (dsc: DynamicSamplingContext) => void): void;
+
/** @inheritdoc */
public on(hook: string, callback: unknown): void {
if (!this._hooks[hook]) {
@@ -400,6 +404,9 @@ export abstract class BaseClient implements Client {
/** @inheritdoc */
public emit(hook: 'beforeAddBreadcrumb', breadcrumb: Breadcrumb, hint?: BreadcrumbHint): void;
+ /** @inheritdoc */
+ public emit(hook: 'createDsc', dsc: DynamicSamplingContext): void;
+
/** @inheritdoc */
public emit(hook: string, ...rest: unknown[]): void {
if (this._hooks[hook]) {
diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts
index b1e6e74195be..9dcee22fb888 100644
--- a/packages/core/src/tracing/transaction.ts
+++ b/packages/core/src/tracing/transaction.ts
@@ -276,6 +276,8 @@ export class Transaction extends SpanClass implements TransactionInterface {
// Uncomment if we want to make DSC immutable
// this._frozenDynamicSamplingContext = dsc;
+ client.emit && client.emit('createDsc', dsc);
+
return dsc;
}
}
diff --git a/packages/replay/src/util/addGlobalListeners.ts b/packages/replay/src/util/addGlobalListeners.ts
index fc68f322d090..46ba18bb9ed3 100644
--- a/packages/replay/src/util/addGlobalListeners.ts
+++ b/packages/replay/src/util/addGlobalListeners.ts
@@ -1,5 +1,6 @@
import type { BaseClient } from '@sentry/core';
import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core';
+import type { Client, DynamicSamplingContext } from '@sentry/types';
import { addInstrumentationHandler } from '@sentry/utils';
import { handleAfterSendEvent } from '../coreHandlers/handleAfterSendEvent';
@@ -25,15 +26,23 @@ export function addGlobalListeners(replay: ReplayContainer): void {
addInstrumentationHandler('history', handleHistorySpanListener(replay));
handleNetworkBreadcrumbs(replay);
- // If a custom client has no hooks yet, we continue to use the "old" implementation
- const hasHooks = !!(client && client.on);
-
// Tag all (non replay) events that get sent to Sentry with the current
// replay ID so that we can reference them later in the UI
- addGlobalEventProcessor(handleGlobalEventListener(replay, !hasHooks));
+ addGlobalEventProcessor(handleGlobalEventListener(replay, !hasHooks(client)));
- if (hasHooks) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (client as BaseClient).on('afterSendEvent', handleAfterSendEvent(replay));
+ // If a custom client has no hooks yet, we continue to use the "old" implementation
+ if (hasHooks(client)) {
+ client.on('afterSendEvent', handleAfterSendEvent(replay));
+ client.on('createDsc', (dsc: DynamicSamplingContext) => {
+ const replayId = replay.getSessionId();
+ if (replayId) {
+ dsc.replay_id = replayId;
+ }
+ });
}
}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function hasHooks(client: Client | undefined): client is BaseClient {
+ return !!(client && client.on);
+}
diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts
index 35b37f868d85..d455c4a7590b 100644
--- a/packages/types/src/client.ts
+++ b/packages/types/src/client.ts
@@ -2,7 +2,7 @@ import type { Breadcrumb, BreadcrumbHint } from './breadcrumb';
import type { EventDropReason } from './clientreport';
import type { DataCategory } from './datacategory';
import type { DsnComponents } from './dsn';
-import type { Envelope } from './envelope';
+import type { DynamicSamplingContext, Envelope } from './envelope';
import type { Event, EventHint } from './event';
import type { Integration, IntegrationClass } from './integration';
import type { ClientOptions } from './options';
@@ -177,6 +177,11 @@ export interface Client {
*/
on?(hook: 'beforeAddBreadcrumb', callback: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => void): void;
+ /**
+ * Register a callback whena DSC (Dynamic Sampling Context) is created.
+ */
+ on?(hook: 'createDsc', callback: (dsc: DynamicSamplingContext) => void): void;
+
/**
* Fire a hook event for transaction start and finish. Expects to be given a transaction as the
* second argument.
@@ -196,7 +201,12 @@ export interface Client {
emit?(hook: 'afterSendEvent', event: Event, sendResponse: TransportMakeRequestResponse | void): void;
/**
- * Fire a hook for when a bredacrumb is added. Expects the breadcrumb as second argument.
+ * Fire a hook for when a breadcrumb is added. Expects the breadcrumb as second argument.
*/
emit?(hook: 'beforeAddBreadcrumb', breadcrumb: Breadcrumb, hint?: BreadcrumbHint): void;
+
+ /**
+ * Fire a hook for when a DSC (Dynamic Sampling Context) is created. Expects the DSC as second argument.
+ */
+ emit?(hook: 'createDsc', dsc: DynamicSamplingContext): void;
}
diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts
index 60d67b89d0da..2234317ef8ce 100644
--- a/packages/types/src/envelope.ts
+++ b/packages/types/src/envelope.ts
@@ -18,6 +18,7 @@ export type DynamicSamplingContext = {
environment?: string;
transaction?: string;
user_segment?: string;
+ replay_id?: string;
};
export type EnvelopeItemType =
From b0be4dddc3b07e61167134a95c03f8ea15390ec6 Mon Sep 17 00:00:00 2001
From: Tim Fish
Date: Thu, 23 Mar 2023 10:02:33 +0000
Subject: [PATCH 30/34] feat(ember): Remove `@sentry/tracing` dependency from
Ember SDK (#7583)
---
.../ember/addon/instance-initializers/sentry-performance.ts | 6 +++---
packages/ember/package.json | 1 -
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts
index ac3c08e46079..0001e6c5f97c 100644
--- a/packages/ember/addon/instance-initializers/sentry-performance.ts
+++ b/packages/ember/addon/instance-initializers/sentry-performance.ts
@@ -386,7 +386,7 @@ export async function instrumentForPerformance(appInstance: ApplicationInstance)
// Maintaining backwards compatibility with config.browserTracingOptions, but passing it with Sentry options is preferred.
const browserTracingOptions = config.browserTracingOptions || config.sentry.browserTracingOptions || {};
- const tracing = await import('@sentry/tracing');
+ const { BrowserTracing } = await import('@sentry/browser');
const idleTimeout = config.transitionTimeout || 5000;
@@ -394,7 +394,7 @@ export async function instrumentForPerformance(appInstance: ApplicationInstance)
sentryConfig['integrations'] = [
...existingIntegrations,
- new tracing.Integrations.BrowserTracing({
+ new BrowserTracing({
routingInstrumentation: (customStartTransaction, startTransactionOnPageLoad) => {
const routerMain = appInstance.lookup('router:main');
let routerService = appInstance.lookup('service:router') as
@@ -421,7 +421,7 @@ export async function instrumentForPerformance(appInstance: ApplicationInstance)
];
class FakeBrowserTracingClass {
- static id = tracing.BROWSER_TRACING_INTEGRATION_ID;
+ static id = 'BrowserTracing';
public name = FakeBrowserTracingClass.id;
setupOnce() {
// noop - We're just faking this class for a lookup
diff --git a/packages/ember/package.json b/packages/ember/package.json
index 2ac858180668..5ba2b5dcfead 100644
--- a/packages/ember/package.json
+++ b/packages/ember/package.json
@@ -30,7 +30,6 @@
"dependencies": {
"@embroider/macros": "^1.9.0",
"@sentry/browser": "7.44.2",
- "@sentry/tracing": "7.44.2",
"@sentry/types": "7.44.2",
"@sentry/utils": "7.44.2",
"ember-auto-import": "^1.12.1 || ^2.4.3",
From f1128bd63e50a734d19876badfb1098a0f7bccc7 Mon Sep 17 00:00:00 2001
From: Lukas Stracke
Date: Thu, 23 Mar 2023 12:04:13 +0100
Subject: [PATCH 31/34] fix(sveltekit): Termporarily disable serverside load
tracing (#7587)
We discovered that our serverside `handleLoadWithSentry` wrapper caused two distinct transactions being created on page loads - one for the page load request and one for the `load` function (if one exists for that page). This shouldn't happen. We need to investigate how to fix this.
As a short-term solution, we disable tracing on the serverside load wrapper to avoid quota usage increase for our users.
---
packages/sveltekit/src/index.types.ts | 4 +-
packages/sveltekit/src/server/load.ts | 46 ++++++++-------------
packages/sveltekit/test/server/load.test.ts | 3 +-
3 files changed, 21 insertions(+), 32 deletions(-)
diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts
index 5835b6863b61..1c1c0576729f 100644
--- a/packages/sveltekit/src/index.types.ts
+++ b/packages/sveltekit/src/index.types.ts
@@ -9,7 +9,7 @@ export * from './server';
import type { Integration, Options, StackParser } from '@sentry/types';
// eslint-disable-next-line import/no-unresolved
-import type { HandleClientError, HandleServerError, ServerLoad } from '@sveltejs/kit';
+import type { HandleClientError, HandleServerError, Load, ServerLoad } from '@sveltejs/kit';
import type * as clientSdk from './client';
import type * as serverSdk from './server';
@@ -21,7 +21,7 @@ export declare function handleErrorWithSentry;
-export declare function wrapLoadWithSentry(origLoad: S): S;
+export declare function wrapLoadWithSentry(origLoad: S): S;
// We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere.
export declare const Integrations: typeof clientSdk.Integrations & typeof serverSdk.Integrations;
diff --git a/packages/sveltekit/src/server/load.ts b/packages/sveltekit/src/server/load.ts
index 5e773365e4a4..6cd45704d601 100644
--- a/packages/sveltekit/src/server/load.ts
+++ b/packages/sveltekit/src/server/load.ts
@@ -1,13 +1,7 @@
/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
-import { trace } from '@sentry/core';
import { captureException } from '@sentry/node';
-import {
- addExceptionMechanism,
- baggageHeaderToDynamicSamplingContext,
- extractTraceparentData,
- objectify,
-} from '@sentry/utils';
-import type { HttpError, ServerLoad } from '@sveltejs/kit';
+import { addExceptionMechanism, isThenable, objectify } from '@sentry/utils';
+import type { HttpError, Load, ServerLoad } from '@sveltejs/kit';
import * as domain from 'domain';
function isHttpError(err: unknown): err is HttpError {
@@ -49,32 +43,26 @@ function sendErrorToSentry(e: unknown): unknown {
*
* @param origLoad SvelteKit user defined load function
*/
-export function wrapLoadWithSentry(origLoad: ServerLoad): ServerLoad {
+export function wrapLoadWithSentry(origLoad: T): T {
return new Proxy(origLoad, {
apply: (wrappingTarget, thisArg, args: Parameters) => {
return domain.create().bind(() => {
- const [event] = args;
+ let maybePromiseResult: ReturnType;
- const sentryTraceHeader = event.request.headers.get('sentry-trace');
- const baggageHeader = event.request.headers.get('baggage');
- const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined;
- const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader);
+ try {
+ maybePromiseResult = wrappingTarget.apply(thisArg, args);
+ } catch (e) {
+ sendErrorToSentry(e);
+ throw e;
+ }
- const routeId = event.route.id;
- return trace(
- {
- op: 'function.sveltekit.load',
- name: routeId ? routeId : event.url.pathname,
- status: 'ok',
- ...traceparentData,
- metadata: {
- source: routeId ? 'route' : 'url',
- dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
- },
- },
- () => wrappingTarget.apply(thisArg, args),
- sendErrorToSentry,
- );
+ if (isThenable(maybePromiseResult)) {
+ Promise.resolve(maybePromiseResult).then(null, e => {
+ sendErrorToSentry(e);
+ });
+ }
+
+ return maybePromiseResult;
})();
},
});
diff --git a/packages/sveltekit/test/server/load.test.ts b/packages/sveltekit/test/server/load.test.ts
index 81b067689093..9278215074c1 100644
--- a/packages/sveltekit/test/server/load.test.ts
+++ b/packages/sveltekit/test/server/load.test.ts
@@ -101,7 +101,8 @@ describe('wrapLoadWithSentry', () => {
expect(mockCaptureException).toHaveBeenCalledTimes(1);
});
- it('calls trace function', async () => {
+ // TODO: enable this once we figured out how tracing the load function doesn't result in creating a new transaction
+ it.skip('calls trace function', async () => {
async function load({ params }: Parameters[0]): Promise> {
return {
post: params.id,
From 1121507127d62f65bedfe514a0f802c08fa679ad Mon Sep 17 00:00:00 2001
From: Lukas Stracke
Date: Thu, 23 Mar 2023 12:04:32 +0100
Subject: [PATCH 32/34] fix(sveltekit): Handle same origin and destination
navigations correctly (#7584)
Previously our Kit routing instrumentation used the parameterized route id to determine if the origin and destination of the navigation were identical (in which case we don't want to create a navigation transaction).
This caused navigations from e.g. `/users/123` to `users/456` to not create a transaction because their route id was identical. This PR fixes this bug by using the raw URL which we also get from the `navigating` store emissions.
Furthermore, this PR fixes the transaction source which previously was always set to `route`, even if no route id was available.
---
packages/sveltekit/src/client/router.ts | 25 ++++--
packages/sveltekit/test/client/router.test.ts | 81 +++++++++++++++----
2 files changed, 83 insertions(+), 23 deletions(-)
diff --git a/packages/sveltekit/src/client/router.ts b/packages/sveltekit/src/client/router.ts
index c4cb7a95c5cf..64125bdbdbd4 100644
--- a/packages/sveltekit/src/client/router.ts
+++ b/packages/sveltekit/src/client/router.ts
@@ -42,6 +42,9 @@ function instrumentPageload(startTransactionFn: (context: TransactionContext) =>
tags: {
...DEFAULT_TAGS,
},
+ metadata: {
+ source: 'url',
+ },
});
page.subscribe(page => {
@@ -76,20 +79,30 @@ function instrumentNavigations(startTransactionFn: (context: TransactionContext)
return;
}
- const routeDestination = navigation.to && navigation.to.route.id;
- const routeOrigin = navigation.from && navigation.from.route.id;
+ const from = navigation.from;
+ const to = navigation.to;
+
+ // for the origin we can fall back to window.location.pathname because in this emission, it still is set to the origin path
+ const rawRouteOrigin = (from && from.url.pathname) || (WINDOW && WINDOW.location && WINDOW.location.pathname);
- if (routeOrigin === routeDestination) {
+ const rawRouteDestination = to && to.url.pathname;
+
+ // We don't want to create transactions for navigations of same origin and destination.
+ // We need to look at the raw URL here because parameterized routes can still differ in their raw parameters.
+ if (rawRouteOrigin === rawRouteDestination) {
return;
}
+ const parameterizedRouteOrigin = from && from.route.id;
+ const parameterizedRouteDestination = to && to.route.id;
+
activeTransaction = getActiveTransaction();
if (!activeTransaction) {
activeTransaction = startTransactionFn({
- name: routeDestination || (WINDOW && WINDOW.location && WINDOW.location.pathname),
+ name: parameterizedRouteDestination || rawRouteDestination || 'unknown',
op: 'navigation',
- metadata: { source: 'route' },
+ metadata: { source: parameterizedRouteDestination ? 'route' : 'url' },
tags: {
...DEFAULT_TAGS,
},
@@ -105,7 +118,7 @@ function instrumentNavigations(startTransactionFn: (context: TransactionContext)
op: 'ui.sveltekit.routing',
description: 'SvelteKit Route Change',
});
- activeTransaction.setTag('from', routeOrigin);
+ activeTransaction.setTag('from', parameterizedRouteOrigin);
}
});
}
diff --git a/packages/sveltekit/test/client/router.test.ts b/packages/sveltekit/test/client/router.test.ts
index a517274ea505..0b95a7195176 100644
--- a/packages/sveltekit/test/client/router.test.ts
+++ b/packages/sveltekit/test/client/router.test.ts
@@ -57,6 +57,9 @@ describe('sveltekitRoutingInstrumentation', () => {
tags: {
'routing.instrumentation': '@sentry/sveltekit',
},
+ metadata: {
+ source: 'url',
+ },
});
// We emit an update to the `page` store to simulate the SvelteKit router lifecycle
@@ -73,15 +76,15 @@ describe('sveltekitRoutingInstrumentation', () => {
expect(mockedStartTransaction).toHaveBeenCalledTimes(0);
});
- it("doesn't starts a navigation transaction when `startTransactionOnLocationChange` is false", () => {
+ it("doesn't start a navigation transaction when `startTransactionOnLocationChange` is false", () => {
svelteKitRoutingInstrumentation(mockedStartTransaction, false, false);
// We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle
// @ts-ignore This is fine because we testUtils/stores.ts defines `navigating` as a writable store
- navigating.set(
- { from: { route: { id: 'testNavigationOrigin' } } },
- { to: { route: { id: 'testNavigationDestination' } } },
- );
+ navigating.set({
+ from: { route: { id: '/users' }, url: { pathname: '/users' } },
+ to: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } },
+ });
// This should update the transaction name with the parameterized route:
expect(mockedStartTransaction).toHaveBeenCalledTimes(0);
@@ -93,14 +96,14 @@ describe('sveltekitRoutingInstrumentation', () => {
// We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle
// @ts-ignore This is fine because we testUtils/stores.ts defines `navigating` as a writable store
navigating.set({
- from: { route: { id: 'testNavigationOrigin' } },
- to: { route: { id: 'testNavigationDestination' } },
+ from: { route: { id: '/users' }, url: { pathname: '/users' } },
+ to: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } },
});
// This should update the transaction name with the parameterized route:
expect(mockedStartTransaction).toHaveBeenCalledTimes(1);
expect(mockedStartTransaction).toHaveBeenCalledWith({
- name: 'testNavigationDestination',
+ name: '/users/[id]',
op: 'navigation',
metadata: {
source: 'route',
@@ -115,7 +118,7 @@ describe('sveltekitRoutingInstrumentation', () => {
description: 'SvelteKit Route Change',
});
- expect(returnedTransaction?.setTag).toHaveBeenCalledWith('from', 'testNavigationOrigin');
+ expect(returnedTransaction?.setTag).toHaveBeenCalledWith('from', '/users');
// We emit `null` here to simulate the end of the navigation lifecycle
// @ts-ignore this is fine
@@ -124,16 +127,60 @@ describe('sveltekitRoutingInstrumentation', () => {
expect(routingSpanFinishSpy).toHaveBeenCalledTimes(1);
});
- it("doesn't start a navigation transaction if navigation origin and destination are equal", () => {
- svelteKitRoutingInstrumentation(mockedStartTransaction, false, true);
+ describe('handling same origin and destination navigations', () => {
+ it("doesn't start a navigation transaction if the raw navigation origin and destination are equal", () => {
+ svelteKitRoutingInstrumentation(mockedStartTransaction, false, true);
- // We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle
- // @ts-ignore This is fine because we testUtils/stores.ts defines `navigating` as a writable store
- navigating.set({
- from: { route: { id: 'testRoute' } },
- to: { route: { id: 'testRoute' } },
+ // We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle
+ // @ts-ignore This is fine because we testUtils/stores.ts defines `navigating` as a writable store
+ navigating.set({
+ from: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } },
+ to: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } },
+ });
+
+ expect(mockedStartTransaction).toHaveBeenCalledTimes(0);
});
- expect(mockedStartTransaction).toHaveBeenCalledTimes(0);
+ it('starts a navigation transaction if the raw navigation origin and destination are not equal', () => {
+ svelteKitRoutingInstrumentation(mockedStartTransaction, false, true);
+
+ // @ts-ignore This is fine
+ navigating.set({
+ from: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } },
+ to: { route: { id: '/users/[id]' }, url: { pathname: '/users/223412' } },
+ });
+
+ expect(mockedStartTransaction).toHaveBeenCalledTimes(1);
+ expect(mockedStartTransaction).toHaveBeenCalledWith({
+ name: '/users/[id]',
+ op: 'navigation',
+ metadata: {
+ source: 'route',
+ },
+ tags: {
+ 'routing.instrumentation': '@sentry/sveltekit',
+ },
+ });
+
+ expect(returnedTransaction?.startChild).toHaveBeenCalledWith({
+ op: 'ui.sveltekit.routing',
+ description: 'SvelteKit Route Change',
+ });
+
+ expect(returnedTransaction?.setTag).toHaveBeenCalledWith('from', '/users/[id]');
+ });
+
+ it('falls back to `window.location.pathname` to determine the raw origin', () => {
+ svelteKitRoutingInstrumentation(mockedStartTransaction, false, true);
+
+ // window.location.pathame is "/" in tests
+
+ // @ts-ignore This is fine
+ navigating.set({
+ to: { route: {}, url: { pathname: '/' } },
+ });
+
+ expect(mockedStartTransaction).toHaveBeenCalledTimes(0);
+ });
});
});
From 047fbc8777b93c4ae24b5c2feb2e1df3bee73527 Mon Sep 17 00:00:00 2001
From: Lukas Stracke
Date: Thu, 23 Mar 2023 12:54:21 +0100
Subject: [PATCH 33/34] meta: Update CHANGELOG for 7.45.0
---
CHANGELOG.md | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19b48530e607..259312f8f9e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,24 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 7.45.0
+
+- build(cdn): Ensure ES5 bundles do not use non-ES5 code (#7550)
+- feat(core): Add trace function (#7556)
+- feat(hub): Make scope always defined on the hub (#7551)
+- feat(replay): Add `replay_id` to transaction DSC (#7571)
+- feat(replay): Capture fetch body size for replay events (#7524)
+- feat(sveltekit): Add performance monitoring for client load (#7537)
+- feat(sveltekit): Add performance monitoring to Sveltekit server handle (#7532)
+- feat(sveltekit): Add SvelteKit routing instrumentation (#7565)
+- fix(browser): Ensure keepalive flag is correctly set for parallel requests (#7553)
+- fix(core): Ensure `ignoreErrors` only applies to error events (#7573)
+- fix(node): Consider tracing error handler for process exit (#7558)
+- fix(otel): Make sure we use correct hub on finish (#7577)
+- fix(react): Handle case where error.cause already defined (#7557)
+- fix(sveltekit): Handle same origin and destination navigations correctly (#7584)
+- fix(tracing): Account for case where startTransaction returns undefined (#7566)
+
## 7.44.2
- fix(cdn): Fix ES5 CDN bundles (#7544)
From f832d4ede03c65b778c1213cc99ed376c1e64f19 Mon Sep 17 00:00:00 2001
From: Lukas Stracke
Date: Thu, 23 Mar 2023 12:58:01 +0100
Subject: [PATCH 34/34] Update CHANGELOG.md
Co-authored-by: Abhijeet Prasad
---
CHANGELOG.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 259312f8f9e7..3d8824213ccd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,7 +19,6 @@
- fix(node): Consider tracing error handler for process exit (#7558)
- fix(otel): Make sure we use correct hub on finish (#7577)
- fix(react): Handle case where error.cause already defined (#7557)
-- fix(sveltekit): Handle same origin and destination navigations correctly (#7584)
- fix(tracing): Account for case where startTransaction returns undefined (#7566)
## 7.44.2