Skip to content

meta(changelog): Update changelog for 9.21.0 #16342

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@

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

## 9.21.0

- docs: Fix v7 migration link ([#14629](https://github.com/getsentry/sentry-javascript/pull/14629))
- feat(node): Vendor in `@fastify/otel` ([#16328](https://github.com/getsentry/sentry-javascript/pull/16328))
- fix(nestjs): Handle multiple `OnEvent` decorators ([#16306](https://github.com/getsentry/sentry-javascript/pull/16306))
- fix(node): Avoid creating breadcrumbs for suppressed requests ([#16285](https://github.com/getsentry/sentry-javascript/pull/16285))
- fix(remix): Add missing `client` exports to `server` and `cloudflare` entries ([#16341](https://github.com/getsentry/sentry-javascript/pull/16341))

Work in this release was contributed by @phthhieu. Thank you for your contribution!

## 9.20.0

### Important changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,11 @@ export class EventsController {

return { message: 'Events emitted' };
}

@Get('emit-multiple')
async emitMultipleEvents() {
await this.eventsService.emitMultipleEvents();

return { message: 'Events emitted' };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,11 @@ export class EventsService {

return { message: 'Events emitted' };
}

async emitMultipleEvents() {
this.eventEmitter.emit('multiple.first', { data: 'test-first' });
this.eventEmitter.emit('multiple.second', { data: 'test-second' });

return { message: 'Events emitted' };
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import * as Sentry from '@sentry/nestjs';

@Injectable()
export class TestEventListener {
Expand All @@ -13,4 +14,11 @@ export class TestEventListener {
await new Promise(resolve => setTimeout(resolve, 100));
throw new Error('Test error from event handler');
}

@OnEvent('multiple.first')
@OnEvent('multiple.second')
async handleMultipleEvents(payload: any): Promise<void> {
Sentry.setTag(payload.data, true);
await new Promise(resolve => setTimeout(resolve, 100));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,27 @@ test('Event emitter', async () => {
status: 'ok',
});
});

test('Multiple OnEvent decorators', async () => {
const firstTxPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => {
return transactionEvent.transaction === 'event multiple.first|multiple.second';
});
const secondTxPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => {
return transactionEvent.transaction === 'event multiple.first|multiple.second';
});
const rootPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => {
return transactionEvent.transaction === 'GET /events/emit-multiple';
});

const eventsUrl = `http://localhost:3050/events/emit-multiple`;
await fetch(eventsUrl);

const firstTx = await firstTxPromise;
const secondTx = await secondTxPromise;
const rootTx = await rootPromise;

expect(firstTx).toBeDefined();
expect(secondTx).toBeDefined();
// assert that the correct payloads were added
expect(rootTx.tags).toMatchObject({ 'test-first': true, 'test-second': true });
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ async function run() {
await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text());
await fetch(`${process.env.SERVER_URL}/api/v3`).then(res => res.text());

await Sentry.suppressTracing(() => fetch(`${process.env.SERVER_URL}/api/v4`).then(res => res.text()));

Sentry.captureException(new Error('foo'));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createTestServer } from '../../../../utils/server';

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

await createRunner()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ async function run() {
await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`);
await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`);

await Sentry.suppressTracing(() => makeHttpRequest(`${process.env.SERVER_URL}/api/v4`));

Sentry.captureException(new Error('foo'));
}

Expand Down
2 changes: 1 addition & 1 deletion docs/changelog/v7.md
Original file line number Diff line number Diff line change
Expand Up @@ -3714,7 +3714,7 @@ requires changes to certain configuration options or custom clients/integrations
a version of [self-hosted Sentry](https://develop.sentry.dev/self-hosted/) (aka onpremise) older than `20.6.0` then you
will need to [upgrade](https://develop.sentry.dev/self-hosted/releases/).**

For detailed overview of all the changes, please see our [v7 migration guide](./MIGRATION.md#upgrading-from-6x-to-7x).
For detailed overview of all the changes, please see our [v7 migration guide](/docs/migration/v6-to-v7.md).

### Breaking Changes

Expand Down
3 changes: 2 additions & 1 deletion packages/browser/src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ export {
} from './stack-parsers';
export { eventFromException, eventFromMessage, exceptionFromError } from './eventbuilder';
export { createUserFeedbackEnvelope } from './userfeedback';
export { getDefaultIntegrations, forceLoad, init, onLoad, showReportDialog } from './sdk';
export { getDefaultIntegrations, forceLoad, init, onLoad } from './sdk';
export { showReportDialog } from './report-dialog';

export { breadcrumbsIntegration } from './integrations/breadcrumbs';
export { globalHandlersIntegration } from './integrations/globalhandlers';
Expand Down
64 changes: 64 additions & 0 deletions packages/browser/src/report-dialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { ReportDialogOptions } from '@sentry/core';
import { getClient, getCurrentScope, getReportDialogEndpoint, lastEventId, logger } from '@sentry/core';
import { DEBUG_BUILD } from './debug-build';
import { WINDOW } from './helpers';

/**
* Present the user with a report dialog.
*
* @param options Everything is optional, we try to fetch all info need from the current scope.
*/
export function showReportDialog(options: ReportDialogOptions = {}): void {
const optionalDocument = WINDOW.document as Document | undefined;
const injectionPoint = optionalDocument?.head || optionalDocument?.body;

// doesn't work without a document (React Native)
if (!injectionPoint) {
DEBUG_BUILD && logger.error('[showReportDialog] Global document not defined');
return;
}

const scope = getCurrentScope();
const client = getClient();
const dsn = client?.getDsn();

if (!dsn) {
DEBUG_BUILD && logger.error('[showReportDialog] DSN not configured');
return;
}

const mergedOptions = {
...options,
user: {
...scope.getUser(),
...options.user,
},
eventId: options.eventId || lastEventId(),
};

const script = WINDOW.document.createElement('script');
script.async = true;
script.crossOrigin = 'anonymous';
script.src = getReportDialogEndpoint(dsn, mergedOptions);

const { onLoad, onClose } = mergedOptions;

if (onLoad) {
script.onload = onLoad;
}

if (onClose) {
const reportDialogClosedMessageHandler = (event: MessageEvent): void => {
if (event.data === '__sentry_reportdialog_closed__') {
try {
onClose();
} finally {
WINDOW.removeEventListener('message', reportDialogClosedMessageHandler);
}
}
};
WINDOW.addEventListener('message', reportDialogClosedMessageHandler);
}

injectionPoint.appendChild(script);
}
71 changes: 1 addition & 70 deletions packages/browser/src/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import type { Client, Integration, Options, ReportDialogOptions } from '@sentry/core';
import type { Client, Integration, Options } from '@sentry/core';
import {
consoleSandbox,
dedupeIntegration,
functionToStringIntegration,
getCurrentScope,
getIntegrationsToSetup,
getLocationHref,
getReportDialogEndpoint,
inboundFiltersIntegration,
initAndBind,
lastEventId,
logger,
stackParserFromStackParserOptions,
supportsFetch,
Expand Down Expand Up @@ -201,72 +198,6 @@ export function init(browserOptions: BrowserOptions = {}): Client | undefined {
return initAndBind(BrowserClient, clientOptions);
}

/**
* Present the user with a report dialog.
*
* @param options Everything is optional, we try to fetch all info need from the global scope.
*/
export function showReportDialog(options: ReportDialogOptions = {}): void {
// doesn't work without a document (React Native)
if (!WINDOW.document) {
DEBUG_BUILD && logger.error('Global document not defined in showReportDialog call');
return;
}

const scope = getCurrentScope();
const client = scope.getClient();
const dsn = client?.getDsn();

if (!dsn) {
DEBUG_BUILD && logger.error('DSN not configured for showReportDialog call');
return;
}

if (scope) {
options.user = {
...scope.getUser(),
...options.user,
};
}

if (!options.eventId) {
const eventId = lastEventId();
if (eventId) {
options.eventId = eventId;
}
}

const script = WINDOW.document.createElement('script');
script.async = true;
script.crossOrigin = 'anonymous';
script.src = getReportDialogEndpoint(dsn, options);

if (options.onLoad) {
script.onload = options.onLoad;
}

const { onClose } = options;
if (onClose) {
const reportDialogClosedMessageHandler = (event: MessageEvent): void => {
if (event.data === '__sentry_reportdialog_closed__') {
try {
onClose();
} finally {
WINDOW.removeEventListener('message', reportDialogClosedMessageHandler);
}
}
};
WINDOW.addEventListener('message', reportDialogClosedMessageHandler);
}

const injectionPoint = WINDOW.document.head || WINDOW.document.body;
if (injectionPoint) {
injectionPoint.appendChild(script);
} else {
DEBUG_BUILD && logger.error('Not injecting report dialog. No injection point found in HTML');
}
}

/**
* This function is here to be API compatible with the loader.
* @hidden
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ describe('browserTracingIntegration', () => {
getIsolationScope().clear();
getCurrentScope().setClient(undefined);
document.head.innerHTML = '';

// We want to suppress the "Multiple browserTracingIntegration instances are not supported." warnings
vi.spyOn(console, 'warn').mockImplementation(() => {});
});

afterEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,31 +58,46 @@ export class SentryNestEventInstrumentation extends InstrumentationBase {
private _createWrapOnEvent() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function wrapOnEvent(original: any) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function wrappedOnEvent(event: any, options?: any) {
const eventName = Array.isArray(event)
? event.join(',')
: typeof event === 'string' || typeof event === 'symbol'
? event.toString()
: '<unknown_event>';

return function wrappedOnEvent(event: unknown, options?: unknown) {
// Get the original decorator result
const decoratorResult = original(event, options);

// Return a new decorator function that wraps the handler
return function (target: OnEventTarget, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
if (!descriptor.value || typeof descriptor.value !== 'function' || target.__SENTRY_INTERNAL__) {
return (target: OnEventTarget, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
if (
!descriptor.value ||
typeof descriptor.value !== 'function' ||
target.__SENTRY_INTERNAL__ ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
descriptor.value.__SENTRY_INSTRUMENTED__
) {
return decoratorResult(target, propertyKey, descriptor);
}

// Get the original handler
const originalHandler = descriptor.value;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const handlerName = originalHandler.name || propertyKey;
let eventName = typeof event === 'string' ? event : String(event);

// Instrument the actual handler
descriptor.value = async function (...args: unknown[]) {
// When multiple @OnEvent decorators are used on a single method, we need to get all event names
// from the reflector metadata as there is no information during execution which event triggered it
if (Reflect.getMetadataKeys(descriptor.value).includes('EVENT_LISTENER_METADATA')) {
const eventData = Reflect.getMetadata('EVENT_LISTENER_METADATA', descriptor.value);
if (Array.isArray(eventData)) {
eventName = eventData
.map((data: unknown) => {
if (data && typeof data === 'object' && 'event' in data && data.event) {
return data.event;
}
return '';
})
.reverse() // decorators are evaluated bottom to top
.join('|');
}
}

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

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
descriptor.value.__SENTRY_INSTRUMENTED__ = true;

// Preserve the original function name
Object.defineProperty(descriptor.value, 'name', {
value: handlerName,
Expand Down
1 change: 1 addition & 0 deletions packages/nestjs/test/integrations/nest.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'reflect-metadata';
import * as core from '@sentry/core';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { isPatched } from '../../src/integrations/helpers';
Expand Down
4 changes: 2 additions & 2 deletions packages/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
"access": "public"
},
"dependencies": {
"@fastify/otel": "https://codeload.github.com/getsentry/fastify-otel/tar.gz/ae3088d65e286bdc94ac5d722573537d6a6671bb",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/context-async-hooks": "^1.30.1",
"@opentelemetry/core": "^1.30.1",
Expand Down Expand Up @@ -98,7 +97,8 @@
"@prisma/instrumentation": "6.7.0",
"@sentry/core": "9.20.0",
"@sentry/opentelemetry": "9.20.0",
"import-in-the-middle": "^1.13.1"
"import-in-the-middle": "^1.13.1",
"minimatch": "^9.0.0"
},
"devDependencies": {
"@types/node": "^18.19.1"
Expand Down
Loading
Loading