Skip to content

Commit b8916bb

Browse files
authored
Merge pull request #16342 from getsentry/prepare-release/9.21.0
meta(changelog): Update changelog for 9.21.0
2 parents 408b7d8 + 8902822 commit b8916bb

File tree

25 files changed

+797
-181
lines changed

25 files changed

+797
-181
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@
1010

1111
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
1212

13+
## 9.21.0
14+
15+
- docs: Fix v7 migration link ([#14629](https://github.com/getsentry/sentry-javascript/pull/14629))
16+
- feat(node): Vendor in `@fastify/otel` ([#16328](https://github.com/getsentry/sentry-javascript/pull/16328))
17+
- fix(nestjs): Handle multiple `OnEvent` decorators ([#16306](https://github.com/getsentry/sentry-javascript/pull/16306))
18+
- fix(node): Avoid creating breadcrumbs for suppressed requests ([#16285](https://github.com/getsentry/sentry-javascript/pull/16285))
19+
- fix(remix): Add missing `client` exports to `server` and `cloudflare` entries ([#16341](https://github.com/getsentry/sentry-javascript/pull/16341))
20+
21+
Work in this release was contributed by @phthhieu. Thank you for your contribution!
22+
1323
## 9.20.0
1424

1525
### Important changes

dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.controller.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,11 @@ export class EventsController {
1111

1212
return { message: 'Events emitted' };
1313
}
14+
15+
@Get('emit-multiple')
16+
async emitMultipleEvents() {
17+
await this.eventsService.emitMultipleEvents();
18+
19+
return { message: 'Events emitted' };
20+
}
1421
}

dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.service.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,11 @@ export class EventsService {
1111

1212
return { message: 'Events emitted' };
1313
}
14+
15+
async emitMultipleEvents() {
16+
this.eventEmitter.emit('multiple.first', { data: 'test-first' });
17+
this.eventEmitter.emit('multiple.second', { data: 'test-second' });
18+
19+
return { message: 'Events emitted' };
20+
}
1421
}

dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/listeners/test-event.listener.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Injectable } from '@nestjs/common';
22
import { OnEvent } from '@nestjs/event-emitter';
3+
import * as Sentry from '@sentry/nestjs';
34

45
@Injectable()
56
export class TestEventListener {
@@ -13,4 +14,11 @@ export class TestEventListener {
1314
await new Promise(resolve => setTimeout(resolve, 100));
1415
throw new Error('Test error from event handler');
1516
}
17+
18+
@OnEvent('multiple.first')
19+
@OnEvent('multiple.second')
20+
async handleMultipleEvents(payload: any): Promise<void> {
21+
Sentry.setTag(payload.data, true);
22+
await new Promise(resolve => setTimeout(resolve, 100));
23+
}
1624
}

dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,27 @@ test('Event emitter', async () => {
4040
status: 'ok',
4141
});
4242
});
43+
44+
test('Multiple OnEvent decorators', async () => {
45+
const firstTxPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => {
46+
return transactionEvent.transaction === 'event multiple.first|multiple.second';
47+
});
48+
const secondTxPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => {
49+
return transactionEvent.transaction === 'event multiple.first|multiple.second';
50+
});
51+
const rootPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => {
52+
return transactionEvent.transaction === 'GET /events/emit-multiple';
53+
});
54+
55+
const eventsUrl = `http://localhost:3050/events/emit-multiple`;
56+
await fetch(eventsUrl);
57+
58+
const firstTx = await firstTxPromise;
59+
const secondTx = await secondTxPromise;
60+
const rootTx = await rootPromise;
61+
62+
expect(firstTx).toBeDefined();
63+
expect(secondTx).toBeDefined();
64+
// assert that the correct payloads were added
65+
expect(rootTx.tags).toMatchObject({ 'test-first': true, 'test-second': true });
66+
});

dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/scenario.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ async function run() {
88
await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text());
99
await fetch(`${process.env.SERVER_URL}/api/v3`).then(res => res.text());
1010

11+
await Sentry.suppressTracing(() => fetch(`${process.env.SERVER_URL}/api/v4`).then(res => res.text()));
12+
1113
Sentry.captureException(new Error('foo'));
1214
}
1315

dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { createTestServer } from '../../../../utils/server';
44

55
describe('outgoing fetch', () => {
66
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
7-
test('outgoing fetch requests create breadcrumbs xxx', async () => {
7+
test('outgoing fetch requests create breadcrumbs', async () => {
88
const [SERVER_URL, closeTestServer] = await createTestServer().start();
99

1010
await createRunner()

dev-packages/node-integration-tests/suites/tracing/requests/http-breadcrumbs/scenario.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ async function run() {
99
await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`);
1010
await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`);
1111

12+
await Sentry.suppressTracing(() => makeHttpRequest(`${process.env.SERVER_URL}/api/v4`));
13+
1214
Sentry.captureException(new Error('foo'));
1315
}
1416

docs/changelog/v7.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3714,7 +3714,7 @@ requires changes to certain configuration options or custom clients/integrations
37143714
a version of [self-hosted Sentry](https://develop.sentry.dev/self-hosted/) (aka onpremise) older than `20.6.0` then you
37153715
will need to [upgrade](https://develop.sentry.dev/self-hosted/releases/).**
37163716
3717-
For detailed overview of all the changes, please see our [v7 migration guide](./MIGRATION.md#upgrading-from-6x-to-7x).
3717+
For detailed overview of all the changes, please see our [v7 migration guide](/docs/migration/v6-to-v7.md).
37183718
37193719
### Breaking Changes
37203720

packages/browser/src/exports.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ export {
8484
} from './stack-parsers';
8585
export { eventFromException, eventFromMessage, exceptionFromError } from './eventbuilder';
8686
export { createUserFeedbackEnvelope } from './userfeedback';
87-
export { getDefaultIntegrations, forceLoad, init, onLoad, showReportDialog } from './sdk';
87+
export { getDefaultIntegrations, forceLoad, init, onLoad } from './sdk';
88+
export { showReportDialog } from './report-dialog';
8889

8990
export { breadcrumbsIntegration } from './integrations/breadcrumbs';
9091
export { globalHandlersIntegration } from './integrations/globalhandlers';

packages/browser/src/report-dialog.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { ReportDialogOptions } from '@sentry/core';
2+
import { getClient, getCurrentScope, getReportDialogEndpoint, lastEventId, logger } from '@sentry/core';
3+
import { DEBUG_BUILD } from './debug-build';
4+
import { WINDOW } from './helpers';
5+
6+
/**
7+
* Present the user with a report dialog.
8+
*
9+
* @param options Everything is optional, we try to fetch all info need from the current scope.
10+
*/
11+
export function showReportDialog(options: ReportDialogOptions = {}): void {
12+
const optionalDocument = WINDOW.document as Document | undefined;
13+
const injectionPoint = optionalDocument?.head || optionalDocument?.body;
14+
15+
// doesn't work without a document (React Native)
16+
if (!injectionPoint) {
17+
DEBUG_BUILD && logger.error('[showReportDialog] Global document not defined');
18+
return;
19+
}
20+
21+
const scope = getCurrentScope();
22+
const client = getClient();
23+
const dsn = client?.getDsn();
24+
25+
if (!dsn) {
26+
DEBUG_BUILD && logger.error('[showReportDialog] DSN not configured');
27+
return;
28+
}
29+
30+
const mergedOptions = {
31+
...options,
32+
user: {
33+
...scope.getUser(),
34+
...options.user,
35+
},
36+
eventId: options.eventId || lastEventId(),
37+
};
38+
39+
const script = WINDOW.document.createElement('script');
40+
script.async = true;
41+
script.crossOrigin = 'anonymous';
42+
script.src = getReportDialogEndpoint(dsn, mergedOptions);
43+
44+
const { onLoad, onClose } = mergedOptions;
45+
46+
if (onLoad) {
47+
script.onload = onLoad;
48+
}
49+
50+
if (onClose) {
51+
const reportDialogClosedMessageHandler = (event: MessageEvent): void => {
52+
if (event.data === '__sentry_reportdialog_closed__') {
53+
try {
54+
onClose();
55+
} finally {
56+
WINDOW.removeEventListener('message', reportDialogClosedMessageHandler);
57+
}
58+
}
59+
};
60+
WINDOW.addEventListener('message', reportDialogClosedMessageHandler);
61+
}
62+
63+
injectionPoint.appendChild(script);
64+
}

packages/browser/src/sdk.ts

Lines changed: 1 addition & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
import type { Client, Integration, Options, ReportDialogOptions } from '@sentry/core';
1+
import type { Client, Integration, Options } from '@sentry/core';
22
import {
33
consoleSandbox,
44
dedupeIntegration,
55
functionToStringIntegration,
6-
getCurrentScope,
76
getIntegrationsToSetup,
87
getLocationHref,
9-
getReportDialogEndpoint,
108
inboundFiltersIntegration,
119
initAndBind,
12-
lastEventId,
1310
logger,
1411
stackParserFromStackParserOptions,
1512
supportsFetch,
@@ -201,72 +198,6 @@ export function init(browserOptions: BrowserOptions = {}): Client | undefined {
201198
return initAndBind(BrowserClient, clientOptions);
202199
}
203200

204-
/**
205-
* Present the user with a report dialog.
206-
*
207-
* @param options Everything is optional, we try to fetch all info need from the global scope.
208-
*/
209-
export function showReportDialog(options: ReportDialogOptions = {}): void {
210-
// doesn't work without a document (React Native)
211-
if (!WINDOW.document) {
212-
DEBUG_BUILD && logger.error('Global document not defined in showReportDialog call');
213-
return;
214-
}
215-
216-
const scope = getCurrentScope();
217-
const client = scope.getClient();
218-
const dsn = client?.getDsn();
219-
220-
if (!dsn) {
221-
DEBUG_BUILD && logger.error('DSN not configured for showReportDialog call');
222-
return;
223-
}
224-
225-
if (scope) {
226-
options.user = {
227-
...scope.getUser(),
228-
...options.user,
229-
};
230-
}
231-
232-
if (!options.eventId) {
233-
const eventId = lastEventId();
234-
if (eventId) {
235-
options.eventId = eventId;
236-
}
237-
}
238-
239-
const script = WINDOW.document.createElement('script');
240-
script.async = true;
241-
script.crossOrigin = 'anonymous';
242-
script.src = getReportDialogEndpoint(dsn, options);
243-
244-
if (options.onLoad) {
245-
script.onload = options.onLoad;
246-
}
247-
248-
const { onClose } = options;
249-
if (onClose) {
250-
const reportDialogClosedMessageHandler = (event: MessageEvent): void => {
251-
if (event.data === '__sentry_reportdialog_closed__') {
252-
try {
253-
onClose();
254-
} finally {
255-
WINDOW.removeEventListener('message', reportDialogClosedMessageHandler);
256-
}
257-
}
258-
};
259-
WINDOW.addEventListener('message', reportDialogClosedMessageHandler);
260-
}
261-
262-
const injectionPoint = WINDOW.document.head || WINDOW.document.body;
263-
if (injectionPoint) {
264-
injectionPoint.appendChild(script);
265-
} else {
266-
DEBUG_BUILD && logger.error('Not injecting report dialog. No injection point found in HTML');
267-
}
268-
}
269-
270201
/**
271202
* This function is here to be API compatible with the loader.
272203
* @hidden

packages/browser/test/tracing/browserTracingIntegration.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ describe('browserTracingIntegration', () => {
7171
getIsolationScope().clear();
7272
getCurrentScope().setClient(undefined);
7373
document.head.innerHTML = '';
74+
75+
// We want to suppress the "Multiple browserTracingIntegration instances are not supported." warnings
76+
vi.spyOn(console, 'warn').mockImplementation(() => {});
7477
});
7578

7679
afterEach(() => {

packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,31 +58,46 @@ export class SentryNestEventInstrumentation extends InstrumentationBase {
5858
private _createWrapOnEvent() {
5959
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6060
return function wrapOnEvent(original: any) {
61-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
62-
return function wrappedOnEvent(event: any, options?: any) {
63-
const eventName = Array.isArray(event)
64-
? event.join(',')
65-
: typeof event === 'string' || typeof event === 'symbol'
66-
? event.toString()
67-
: '<unknown_event>';
68-
61+
return function wrappedOnEvent(event: unknown, options?: unknown) {
6962
// Get the original decorator result
7063
const decoratorResult = original(event, options);
7164

7265
// Return a new decorator function that wraps the handler
73-
return function (target: OnEventTarget, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
74-
if (!descriptor.value || typeof descriptor.value !== 'function' || target.__SENTRY_INTERNAL__) {
66+
return (target: OnEventTarget, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
67+
if (
68+
!descriptor.value ||
69+
typeof descriptor.value !== 'function' ||
70+
target.__SENTRY_INTERNAL__ ||
71+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
72+
descriptor.value.__SENTRY_INSTRUMENTED__
73+
) {
7574
return decoratorResult(target, propertyKey, descriptor);
7675
}
7776

78-
// Get the original handler
7977
const originalHandler = descriptor.value;
8078
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
8179
const handlerName = originalHandler.name || propertyKey;
80+
let eventName = typeof event === 'string' ? event : String(event);
81+
82+
// Instrument the actual handler
83+
descriptor.value = async function (...args: unknown[]) {
84+
// When multiple @OnEvent decorators are used on a single method, we need to get all event names
85+
// from the reflector metadata as there is no information during execution which event triggered it
86+
if (Reflect.getMetadataKeys(descriptor.value).includes('EVENT_LISTENER_METADATA')) {
87+
const eventData = Reflect.getMetadata('EVENT_LISTENER_METADATA', descriptor.value);
88+
if (Array.isArray(eventData)) {
89+
eventName = eventData
90+
.map((data: unknown) => {
91+
if (data && typeof data === 'object' && 'event' in data && data.event) {
92+
return data.event;
93+
}
94+
return '';
95+
})
96+
.reverse() // decorators are evaluated bottom to top
97+
.join('|');
98+
}
99+
}
82100

83-
// Instrument the handler
84-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
85-
descriptor.value = async function (...args: any[]) {
86101
return startSpan(getEventSpanOptions(eventName), async () => {
87102
try {
88103
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
@@ -96,6 +111,9 @@ export class SentryNestEventInstrumentation extends InstrumentationBase {
96111
});
97112
};
98113

114+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
115+
descriptor.value.__SENTRY_INSTRUMENTED__ = true;
116+
99117
// Preserve the original function name
100118
Object.defineProperty(descriptor.value, 'name', {
101119
value: handlerName,

packages/nestjs/test/integrations/nest.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'reflect-metadata';
12
import * as core from '@sentry/core';
23
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
34
import { isPatched } from '../../src/integrations/helpers';

packages/node/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@
6565
"access": "public"
6666
},
6767
"dependencies": {
68-
"@fastify/otel": "https://codeload.github.com/getsentry/fastify-otel/tar.gz/ae3088d65e286bdc94ac5d722573537d6a6671bb",
6968
"@opentelemetry/api": "^1.9.0",
7069
"@opentelemetry/context-async-hooks": "^1.30.1",
7170
"@opentelemetry/core": "^1.30.1",
@@ -98,7 +97,8 @@
9897
"@prisma/instrumentation": "6.7.0",
9998
"@sentry/core": "9.20.0",
10099
"@sentry/opentelemetry": "9.20.0",
101-
"import-in-the-middle": "^1.13.1"
100+
"import-in-the-middle": "^1.13.1",
101+
"minimatch": "^9.0.0"
102102
},
103103
"devDependencies": {
104104
"@types/node": "^18.19.1"

0 commit comments

Comments
 (0)