Skip to content

Commit

Permalink
feat: handle captureFeedback errors (#4364)
Browse files Browse the repository at this point in the history
  • Loading branch information
antonis authored Jan 15, 2025
1 parent 08eecba commit 4529b68
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 15 deletions.
29 changes: 16 additions & 13 deletions packages/core/src/js/feedback/FeedbackForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { SendFeedbackParams } from '@sentry/core';
import { captureFeedback, getCurrentScope, lastEventId } from '@sentry/core';
import { captureFeedback, getCurrentScope, lastEventId, logger } from '@sentry/core';
import * as React from 'react';
import type { KeyboardTypeOptions } from 'react-native';
import {
Expand All @@ -20,6 +20,7 @@ import { sentryLogo } from './branding';
import { defaultConfiguration } from './defaults';
import defaultStyles from './FeedbackForm.styles';
import type { FeedbackFormProps, FeedbackFormState, FeedbackFormStyles,FeedbackGeneralConfiguration, FeedbackTextConfiguration } from './FeedbackForm.types';
import { isValidEmail } from './utils';

/**
* @beta
Expand Down Expand Up @@ -50,7 +51,7 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor

public handleFeedbackSubmit: () => void = () => {
const { name, email, description } = this.state;
const { onFormClose } = this.props;
const { onSubmitSuccess, onSubmitError, onFormSubmitted } = this.props;
const text: FeedbackTextConfiguration = this.props;

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

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

onFormClose();
this.setState({ isVisible: false });

captureFeedback(userFeedback);
Alert.alert(text.successMessageText);
try {
this.setState({ isVisible: false });
captureFeedback(userFeedback);
onSubmitSuccess({ name: trimmedName, email: trimmedEmail, message: trimmedDescription, attachments: undefined });
Alert.alert(text.successMessageText);
onFormSubmitted();
} catch (error) {
const errorString = `Feedback form submission failed: ${error}`;
onSubmitError(new Error(errorString));
Alert.alert(text.errorTitle, text.genericError);
logger.error(`Feedback form submission failed: ${error}`);
}
};

/**
Expand Down Expand Up @@ -174,9 +182,4 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
</SafeAreaView>
);
}

private _isValidEmail = (email: string): boolean => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
return emailRegex.test(email);
};
}
28 changes: 28 additions & 0 deletions packages/core/src/js/feedback/FeedbackForm.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { FeedbackFormData } from '@sentry/core';
import type { ImageStyle, TextStyle, ViewStyle } from 'react-native';

/**
Expand Down Expand Up @@ -126,16 +127,43 @@ export interface FeedbackTextConfiguration {
* The error message when the email is invalid
*/
emailError?: string;

/**
* Message when there is a generic error
*/
genericError?: string;
}

/**
* The public callbacks available for the feedback integration
*/
export interface FeedbackCallbacks {
/**
* Callback when form is opened
*/
onFormOpen?: () => void;

/**
* Callback when form is closed and not submitted
*/
onFormClose?: () => void;

/**
* Callback when feedback is successfully submitted
*
* After this you'll see a SuccessMessage on the screen for a moment.
*/
onSubmitSuccess?: (data: FeedbackFormData) => void;

/**
* Callback when feedback is unsuccessfully submitted
*/
onSubmitError?: (error: Error) => void;

/**
* Callback when the feedback form is submitted successfully, and the SuccessMessage is complete, or dismissed
*/
onFormSubmitted?: () => void;
}

/**
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/js/feedback/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ const ERROR_TITLE = 'Error';
const FORM_ERROR = 'Please fill out all required fields.';
const EMAIL_ERROR = 'Please enter a valid email address.';
const SUCCESS_MESSAGE_TEXT = 'Thank you for your report!';
const GENERIC_ERROR_TEXT = 'Unable to send feedback due to an unexpected error.';

export const defaultConfiguration: Partial<FeedbackFormProps> = {
// FeedbackCallbacks
onFormOpen: () => {
// Does nothing by default
},
onFormClose: () => {
if (__DEV__) {
Alert.alert(
Expand All @@ -27,6 +31,20 @@ export const defaultConfiguration: Partial<FeedbackFormProps> = {
);
}
},
onSubmitSuccess: () => {
// Does nothing by default
},
onSubmitError: () => {
// Does nothing by default
},
onFormSubmitted: () => {
if (__DEV__) {
Alert.alert(
'Development note',
'onFormSubmitted callback is not implemented. By default the form is just unmounted.',
);
}
},

// FeedbackGeneralConfiguration
showBranding: true,
Expand All @@ -51,4 +69,5 @@ export const defaultConfiguration: Partial<FeedbackFormProps> = {
formError: FORM_ERROR,
emailError: EMAIL_ERROR,
successMessageText: SUCCESS_MESSAGE_TEXT,
genericError: GENERIC_ERROR_TEXT,
};
4 changes: 4 additions & 0 deletions packages/core/src/js/feedback/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const isValidEmail = (email: string): boolean => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
};
62 changes: 60 additions & 2 deletions packages/core/test/feedback/FeedbackForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { FeedbackForm } from '../../src/js/feedback/FeedbackForm';
import type { FeedbackFormProps, FeedbackFormStyles } from '../../src/js/feedback/FeedbackForm.types';

const mockOnFormClose = jest.fn();
const mockOnSubmitSuccess = jest.fn();
const mockOnFormSubmitted = jest.fn();
const mockOnSubmitError = jest.fn();
const mockGetUser = jest.fn(() => ({
email: '[email protected]',
name: 'Test User',
Expand All @@ -15,6 +18,7 @@ const mockGetUser = jest.fn(() => ({
jest.spyOn(Alert, 'alert');

jest.mock('@sentry/core', () => ({
...jest.requireActual('@sentry/core'),
captureFeedback: jest.fn(),
getCurrentScope: jest.fn(() => ({
getUser: mockGetUser,
Expand All @@ -24,6 +28,9 @@ jest.mock('@sentry/core', () => ({

const defaultProps: FeedbackFormProps = {
onFormClose: mockOnFormClose,
onSubmitSuccess: mockOnSubmitSuccess,
onFormSubmitted: mockOnFormSubmitted,
onSubmitError: mockOnSubmitError,
formTitle: 'Feedback Form',
nameLabel: 'Name Label',
namePlaceholder: 'Name Placeholder',
Expand All @@ -38,6 +45,7 @@ const defaultProps: FeedbackFormProps = {
formError: 'Please fill out all required fields.',
emailError: 'The email address is not valid.',
successMessageText: 'Feedback success',
genericError: 'Generic error',
};

const customStyles: FeedbackFormStyles = {
Expand Down Expand Up @@ -198,7 +206,57 @@ describe('FeedbackForm', () => {
});
});

it('calls onFormClose when the form is submitted successfully', async () => {
it('shows an error message when there is a an error in captureFeedback', async () => {
(captureFeedback as jest.Mock).mockImplementationOnce(() => {
throw new Error('Test error');
});

const { getByPlaceholderText, getByText } = render(<FeedbackForm {...defaultProps} />);

fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe');
fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), '[email protected]');
fireEvent.changeText(getByPlaceholderText(defaultProps.messagePlaceholder), 'This is a feedback message.');

fireEvent.press(getByText(defaultProps.submitButtonLabel));

await waitFor(() => {
expect(Alert.alert).toHaveBeenCalledWith(defaultProps.errorTitle, defaultProps.genericError);
});
});

it('calls onSubmitError when there is an error', async () => {
(captureFeedback as jest.Mock).mockImplementationOnce(() => {
throw new Error('Test error');
});

const { getByPlaceholderText, getByText } = render(<FeedbackForm {...defaultProps} />);

fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe');
fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), '[email protected]');
fireEvent.changeText(getByPlaceholderText(defaultProps.messagePlaceholder), 'This is a feedback message.');

fireEvent.press(getByText(defaultProps.submitButtonLabel));

await waitFor(() => {
expect(mockOnSubmitError).toHaveBeenCalled();
});
});

it('calls onSubmitSuccess when the form is submitted successfully', async () => {
const { getByPlaceholderText, getByText } = render(<FeedbackForm {...defaultProps} />);

fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe');
fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), '[email protected]');
fireEvent.changeText(getByPlaceholderText(defaultProps.messagePlaceholder), 'This is a feedback message.');

fireEvent.press(getByText(defaultProps.submitButtonLabel));

await waitFor(() => {
expect(mockOnSubmitSuccess).toHaveBeenCalled();
});
});

it('calls onFormSubmitted when the form is submitted successfully', async () => {
const { getByPlaceholderText, getByText } = render(<FeedbackForm {...defaultProps} />);

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

await waitFor(() => {
expect(mockOnFormClose).toHaveBeenCalled();
expect(mockOnFormSubmitted).toHaveBeenCalled();
});
});

Expand Down
1 change: 1 addition & 0 deletions samples/react-native/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ const ErrorsTabNavigator = Sentry.withProfiler(
<FeedbackForm
{...props}
onFormClose={props.navigation.goBack}
onFormSubmitted={props.navigation.goBack}
styles={{
submitButton: {
backgroundColor: '#6a1b9a',
Expand Down

0 comments on commit 4529b68

Please sign in to comment.