Skip to content

Commit c99b2ae

Browse files
authored
feat(feedback): Add onClose callback to showReportDialog (#9433) (#9550)
Adds an `onClose` callback to `showReportDialog`. The callback is invoked when a `__sentry_reportdialog_closed__` MessageEvent is received. This is sent from the error page embed via `window.postMessage` and listened to in the sdk.
1 parent 692e9c6 commit c99b2ae

File tree

5 files changed

+86
-0
lines changed

5 files changed

+86
-0
lines changed

packages/browser/src/helpers.ts

+2
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,6 @@ export interface ReportDialogOptions {
180180
successMessage?: string;
181181
/** Callback after reportDialog showed up */
182182
onLoad?(this: void): void;
183+
/** Callback after reportDialog closed */
184+
onClose?(this: void): void;
183185
}

packages/browser/src/sdk.ts

+14
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,20 @@ export function showReportDialog(options: ReportDialogOptions = {}, hub: Hub = g
166166
script.onload = options.onLoad;
167167
}
168168

169+
const { onClose } = options;
170+
if (onClose) {
171+
const reportDialogClosedMessageHandler = (event: MessageEvent): void => {
172+
if (event.data === '__sentry_reportdialog_closed__') {
173+
try {
174+
onClose();
175+
} finally {
176+
WINDOW.removeEventListener('message', reportDialogClosedMessageHandler);
177+
}
178+
}
179+
};
180+
WINDOW.addEventListener('message', reportDialogClosedMessageHandler);
181+
}
182+
169183
const injectionPoint = WINDOW.document.head || WINDOW.document.body;
170184
if (injectionPoint) {
171185
injectionPoint.appendChild(script);

packages/browser/test/unit/index.test.ts

+60
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
Integrations,
1818
Scope,
1919
showReportDialog,
20+
WINDOW,
2021
wrap,
2122
} from '../../src';
2223
import { getDefaultBrowserClientOptions } from './helper/browser-client-options';
@@ -124,6 +125,65 @@ describe('SentryBrowser', () => {
124125
);
125126
});
126127
});
128+
129+
describe('onClose', () => {
130+
const dummyErrorHandler = jest.fn();
131+
beforeEach(() => {
132+
// this prevents jest-environment-jsdom from failing the test
133+
// when an error in `onClose` is thrown
134+
// it does not prevent errors thrown directly inside the test,
135+
// so we don't have to worry about tests passing that should
136+
// otherwise fail
137+
// see: https://github.com/jestjs/jest/blob/main/packages/jest-environment-jsdom/src/index.ts#L95-L115
138+
WINDOW.addEventListener('error', dummyErrorHandler);
139+
});
140+
141+
afterEach(() => {
142+
WINDOW.removeEventListener('error', dummyErrorHandler);
143+
});
144+
145+
const waitForPostMessage = async (message: string) => {
146+
WINDOW.postMessage(message, '*');
147+
await flush(10);
148+
};
149+
150+
it('should call `onClose` when receiving `__sentry_reportdialog_closed__` MessageEvent', async () => {
151+
const onClose = jest.fn();
152+
showReportDialog({ onClose });
153+
154+
await waitForPostMessage('__sentry_reportdialog_closed__');
155+
expect(onClose).toHaveBeenCalledTimes(1);
156+
157+
// ensure the event handler has been removed so onClose is not called again
158+
await waitForPostMessage('__sentry_reportdialog_closed__');
159+
expect(onClose).toHaveBeenCalledTimes(1);
160+
});
161+
162+
it('should call `onClose` only once even if it throws', async () => {
163+
const onClose = jest.fn(() => {
164+
throw new Error();
165+
});
166+
showReportDialog({ onClose });
167+
168+
await waitForPostMessage('__sentry_reportdialog_closed__');
169+
expect(onClose).toHaveBeenCalledTimes(1);
170+
171+
// ensure the event handler has been removed so onClose is not called again
172+
await waitForPostMessage('__sentry_reportdialog_closed__');
173+
expect(onClose).toHaveBeenCalledTimes(1);
174+
});
175+
176+
it('should not call `onClose` for other MessageEvents', async () => {
177+
const onClose = jest.fn();
178+
showReportDialog({ onClose });
179+
180+
await waitForPostMessage('some_message');
181+
expect(onClose).not.toHaveBeenCalled();
182+
183+
await waitForPostMessage('__sentry_reportdialog_closed__');
184+
expect(onClose).toHaveBeenCalledTimes(1);
185+
});
186+
});
127187
});
128188

129189
describe('breadcrumbs', () => {

packages/core/src/api.ts

+4
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ export function getReportDialogEndpoint(
7070
continue;
7171
}
7272

73+
if (key === 'onClose') {
74+
continue;
75+
}
76+
7377
if (key === 'user') {
7478
const user = dialogOptions.user;
7579
if (!user) {

packages/core/test/lib/api.test.ts

+6
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ describe('API', () => {
119119
{ user: undefined },
120120
'https://sentry.io:1234/subpath/api/embed/error-page/?dsn=https://[email protected]:1234/subpath/123',
121121
],
122+
[
123+
'with Public DSN and onClose callback',
124+
dsnPublic,
125+
{ onClose: () => {} },
126+
'https://sentry.io:1234/subpath/api/embed/error-page/?dsn=https://[email protected]:1234/subpath/123',
127+
],
122128
])(
123129
'%s',
124130
(

0 commit comments

Comments
 (0)