Skip to content

Commit 4529b68

Browse files
authored
feat: handle captureFeedback errors (#4364)
1 parent 08eecba commit 4529b68

File tree

6 files changed

+128
-15
lines changed

6 files changed

+128
-15
lines changed

packages/core/src/js/feedback/FeedbackForm.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { SendFeedbackParams } from '@sentry/core';
2-
import { captureFeedback, getCurrentScope, lastEventId } from '@sentry/core';
2+
import { captureFeedback, getCurrentScope, lastEventId, logger } from '@sentry/core';
33
import * as React from 'react';
44
import type { KeyboardTypeOptions } from 'react-native';
55
import {
@@ -20,6 +20,7 @@ import { sentryLogo } from './branding';
2020
import { defaultConfiguration } from './defaults';
2121
import defaultStyles from './FeedbackForm.styles';
2222
import type { FeedbackFormProps, FeedbackFormState, FeedbackFormStyles,FeedbackGeneralConfiguration, FeedbackTextConfiguration } from './FeedbackForm.types';
23+
import { isValidEmail } from './utils';
2324

2425
/**
2526
* @beta
@@ -50,7 +51,7 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
5051

5152
public handleFeedbackSubmit: () => void = () => {
5253
const { name, email, description } = this.state;
53-
const { onFormClose } = this.props;
54+
const { onSubmitSuccess, onSubmitError, onFormSubmitted } = this.props;
5455
const text: FeedbackTextConfiguration = this.props;
5556

5657
const trimmedName = name?.trim();
@@ -62,7 +63,7 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
6263
return;
6364
}
6465

65-
if (this.props.shouldValidateEmail && (this.props.isEmailRequired || trimmedEmail.length > 0) && !this._isValidEmail(trimmedEmail)) {
66+
if (this.props.shouldValidateEmail && (this.props.isEmailRequired || trimmedEmail.length > 0) && !isValidEmail(trimmedEmail)) {
6667
Alert.alert(text.errorTitle, text.emailError);
6768
return;
6869
}
@@ -75,11 +76,18 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
7576
associatedEventId: eventId,
7677
};
7778

78-
onFormClose();
79-
this.setState({ isVisible: false });
80-
81-
captureFeedback(userFeedback);
82-
Alert.alert(text.successMessageText);
79+
try {
80+
this.setState({ isVisible: false });
81+
captureFeedback(userFeedback);
82+
onSubmitSuccess({ name: trimmedName, email: trimmedEmail, message: trimmedDescription, attachments: undefined });
83+
Alert.alert(text.successMessageText);
84+
onFormSubmitted();
85+
} catch (error) {
86+
const errorString = `Feedback form submission failed: ${error}`;
87+
onSubmitError(new Error(errorString));
88+
Alert.alert(text.errorTitle, text.genericError);
89+
logger.error(`Feedback form submission failed: ${error}`);
90+
}
8391
};
8492

8593
/**
@@ -174,9 +182,4 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
174182
</SafeAreaView>
175183
);
176184
}
177-
178-
private _isValidEmail = (email: string): boolean => {
179-
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
180-
return emailRegex.test(email);
181-
};
182185
}

packages/core/src/js/feedback/FeedbackForm.types.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { FeedbackFormData } from '@sentry/core';
12
import type { ImageStyle, TextStyle, ViewStyle } from 'react-native';
23

34
/**
@@ -126,16 +127,43 @@ export interface FeedbackTextConfiguration {
126127
* The error message when the email is invalid
127128
*/
128129
emailError?: string;
130+
131+
/**
132+
* Message when there is a generic error
133+
*/
134+
genericError?: string;
129135
}
130136

131137
/**
132138
* The public callbacks available for the feedback integration
133139
*/
134140
export interface FeedbackCallbacks {
141+
/**
142+
* Callback when form is opened
143+
*/
144+
onFormOpen?: () => void;
145+
135146
/**
136147
* Callback when form is closed and not submitted
137148
*/
138149
onFormClose?: () => void;
150+
151+
/**
152+
* Callback when feedback is successfully submitted
153+
*
154+
* After this you'll see a SuccessMessage on the screen for a moment.
155+
*/
156+
onSubmitSuccess?: (data: FeedbackFormData) => void;
157+
158+
/**
159+
* Callback when feedback is unsuccessfully submitted
160+
*/
161+
onSubmitError?: (error: Error) => void;
162+
163+
/**
164+
* Callback when the feedback form is submitted successfully, and the SuccessMessage is complete, or dismissed
165+
*/
166+
onFormSubmitted?: () => void;
139167
}
140168

141169
/**

packages/core/src/js/feedback/defaults.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ const ERROR_TITLE = 'Error';
1616
const FORM_ERROR = 'Please fill out all required fields.';
1717
const EMAIL_ERROR = 'Please enter a valid email address.';
1818
const SUCCESS_MESSAGE_TEXT = 'Thank you for your report!';
19+
const GENERIC_ERROR_TEXT = 'Unable to send feedback due to an unexpected error.';
1920

2021
export const defaultConfiguration: Partial<FeedbackFormProps> = {
2122
// FeedbackCallbacks
23+
onFormOpen: () => {
24+
// Does nothing by default
25+
},
2226
onFormClose: () => {
2327
if (__DEV__) {
2428
Alert.alert(
@@ -27,6 +31,20 @@ export const defaultConfiguration: Partial<FeedbackFormProps> = {
2731
);
2832
}
2933
},
34+
onSubmitSuccess: () => {
35+
// Does nothing by default
36+
},
37+
onSubmitError: () => {
38+
// Does nothing by default
39+
},
40+
onFormSubmitted: () => {
41+
if (__DEV__) {
42+
Alert.alert(
43+
'Development note',
44+
'onFormSubmitted callback is not implemented. By default the form is just unmounted.',
45+
);
46+
}
47+
},
3048

3149
// FeedbackGeneralConfiguration
3250
showBranding: true,
@@ -51,4 +69,5 @@ export const defaultConfiguration: Partial<FeedbackFormProps> = {
5169
formError: FORM_ERROR,
5270
emailError: EMAIL_ERROR,
5371
successMessageText: SUCCESS_MESSAGE_TEXT,
72+
genericError: GENERIC_ERROR_TEXT,
5473
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const isValidEmail = (email: string): boolean => {
2+
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
3+
return emailRegex.test(email);
4+
};

packages/core/test/feedback/FeedbackForm.test.tsx

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { FeedbackForm } from '../../src/js/feedback/FeedbackForm';
77
import type { FeedbackFormProps, FeedbackFormStyles } from '../../src/js/feedback/FeedbackForm.types';
88

99
const mockOnFormClose = jest.fn();
10+
const mockOnSubmitSuccess = jest.fn();
11+
const mockOnFormSubmitted = jest.fn();
12+
const mockOnSubmitError = jest.fn();
1013
const mockGetUser = jest.fn(() => ({
1114
1215
name: 'Test User',
@@ -15,6 +18,7 @@ const mockGetUser = jest.fn(() => ({
1518
jest.spyOn(Alert, 'alert');
1619

1720
jest.mock('@sentry/core', () => ({
21+
...jest.requireActual('@sentry/core'),
1822
captureFeedback: jest.fn(),
1923
getCurrentScope: jest.fn(() => ({
2024
getUser: mockGetUser,
@@ -24,6 +28,9 @@ jest.mock('@sentry/core', () => ({
2428

2529
const defaultProps: FeedbackFormProps = {
2630
onFormClose: mockOnFormClose,
31+
onSubmitSuccess: mockOnSubmitSuccess,
32+
onFormSubmitted: mockOnFormSubmitted,
33+
onSubmitError: mockOnSubmitError,
2734
formTitle: 'Feedback Form',
2835
nameLabel: 'Name Label',
2936
namePlaceholder: 'Name Placeholder',
@@ -38,6 +45,7 @@ const defaultProps: FeedbackFormProps = {
3845
formError: 'Please fill out all required fields.',
3946
emailError: 'The email address is not valid.',
4047
successMessageText: 'Feedback success',
48+
genericError: 'Generic error',
4149
};
4250

4351
const customStyles: FeedbackFormStyles = {
@@ -198,7 +206,57 @@ describe('FeedbackForm', () => {
198206
});
199207
});
200208

201-
it('calls onFormClose when the form is submitted successfully', async () => {
209+
it('shows an error message when there is a an error in captureFeedback', async () => {
210+
(captureFeedback as jest.Mock).mockImplementationOnce(() => {
211+
throw new Error('Test error');
212+
});
213+
214+
const { getByPlaceholderText, getByText } = render(<FeedbackForm {...defaultProps} />);
215+
216+
fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe');
217+
fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), '[email protected]');
218+
fireEvent.changeText(getByPlaceholderText(defaultProps.messagePlaceholder), 'This is a feedback message.');
219+
220+
fireEvent.press(getByText(defaultProps.submitButtonLabel));
221+
222+
await waitFor(() => {
223+
expect(Alert.alert).toHaveBeenCalledWith(defaultProps.errorTitle, defaultProps.genericError);
224+
});
225+
});
226+
227+
it('calls onSubmitError when there is an error', async () => {
228+
(captureFeedback as jest.Mock).mockImplementationOnce(() => {
229+
throw new Error('Test error');
230+
});
231+
232+
const { getByPlaceholderText, getByText } = render(<FeedbackForm {...defaultProps} />);
233+
234+
fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe');
235+
fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), '[email protected]');
236+
fireEvent.changeText(getByPlaceholderText(defaultProps.messagePlaceholder), 'This is a feedback message.');
237+
238+
fireEvent.press(getByText(defaultProps.submitButtonLabel));
239+
240+
await waitFor(() => {
241+
expect(mockOnSubmitError).toHaveBeenCalled();
242+
});
243+
});
244+
245+
it('calls onSubmitSuccess when the form is submitted successfully', async () => {
246+
const { getByPlaceholderText, getByText } = render(<FeedbackForm {...defaultProps} />);
247+
248+
fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe');
249+
fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), '[email protected]');
250+
fireEvent.changeText(getByPlaceholderText(defaultProps.messagePlaceholder), 'This is a feedback message.');
251+
252+
fireEvent.press(getByText(defaultProps.submitButtonLabel));
253+
254+
await waitFor(() => {
255+
expect(mockOnSubmitSuccess).toHaveBeenCalled();
256+
});
257+
});
258+
259+
it('calls onFormSubmitted when the form is submitted successfully', async () => {
202260
const { getByPlaceholderText, getByText } = render(<FeedbackForm {...defaultProps} />);
203261

204262
fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe');
@@ -208,7 +266,7 @@ describe('FeedbackForm', () => {
208266
fireEvent.press(getByText(defaultProps.submitButtonLabel));
209267

210268
await waitFor(() => {
211-
expect(mockOnFormClose).toHaveBeenCalled();
269+
expect(mockOnFormSubmitted).toHaveBeenCalled();
212270
});
213271
});
214272

samples/react-native/src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ const ErrorsTabNavigator = Sentry.withProfiler(
158158
<FeedbackForm
159159
{...props}
160160
onFormClose={props.navigation.goBack}
161+
onFormSubmitted={props.navigation.goBack}
161162
styles={{
162163
submitButton: {
163164
backgroundColor: '#6a1b9a',

0 commit comments

Comments
 (0)