Skip to content
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

(3.2) Handles Capture feedback form submission errors #4383

Draft
wants to merge 5 commits into
base: antonis/4359-Feedback-Form-NetworkError
Choose a base branch
from
Draft
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
29 changes: 22 additions & 7 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, logger } from '@sentry/core';
import type { EventHint, SendFeedbackParams } from '@sentry/core';
import { getCurrentScope, lastEventId, logger } from '@sentry/core';
import * as React from 'react';
import type { KeyboardTypeOptions } from 'react-native';
import {
Expand All @@ -20,8 +20,18 @@ import { sentryLogo } from './branding';
import { defaultConfiguration } from './defaults';
import defaultStyles from './FeedbackForm.styles';
import type { FeedbackFormProps, FeedbackFormState, FeedbackFormStyles,FeedbackGeneralConfiguration, FeedbackTextConfiguration } from './FeedbackForm.types';
import { sendFeedback } from './sendFeedback';
import { checkInternetConnection, isValidEmail } from './utils';

const submitFeedback = async (feedbackParams: SendFeedbackParams, hint: EventHint, success: () => void, error: (e: string) => void): Promise<void> => {
try {
await sendFeedback(feedbackParams, hint);
success();
} catch (e) {
error(e.toString());
}
};

let feedbackFormHandler: (() => void) | null = null;

const setFeedbackFormHandler = (handler: () => void): void => {
Expand Down Expand Up @@ -110,11 +120,16 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor

// eslint-disable-next-line @typescript-eslint/no-floating-promises
checkInternetConnection(() => { // onConnected
this.setState({ isVisible: false });
captureFeedback(userFeedback);
onSubmitSuccess({ name: trimmedName, email: trimmedEmail, message: trimmedDescription, attachments: undefined });
Alert.alert(text.successMessageText);
onFormSubmitted();
// eslint-disable-next-line @typescript-eslint/no-floating-promises
submitFeedback(userFeedback, undefined, () => { // success
this.setState({ isVisible: false });
onSubmitSuccess({ name: trimmedName, email: trimmedEmail, message: trimmedDescription, attachments: undefined });
Alert.alert(text.successMessageText);
onFormSubmitted();
}, (error: string) => { // error
Alert.alert(text.errorTitle, error);
logger.error(`Feedback form submission failed: ${error}`);
});
}, () => { // onDisconnected
Alert.alert(text.errorTitle, text.networkError);
logger.error(`Feedback form submission failed: ${text.networkError}`);
Expand Down
52 changes: 52 additions & 0 deletions packages/core/src/js/feedback/sendFeedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { Event, EventHint, SendFeedback, SendFeedbackParams, TransportMakeRequestResponse } from '@sentry/core';
import { captureFeedback, getClient, logger } from '@sentry/core';

const FEEDBACK_WIDGET_SOURCE = 'widget';
const FEEDBACK_RESPONSE_TIMEOUT = 5_000;

/**
* Sends feedback to Sentry using Sentry.captureFeedback and waits for the response.
*/
export const sendFeedback: SendFeedback = (
params: SendFeedbackParams,
hint: EventHint & { includeReplay?: boolean } = { includeReplay: true },
): Promise<string> => {
const client = getClient();
if (!client) {
throw new Error('No client setup, cannot send feedback.');
}

const eventId = captureFeedback(
{
source: FEEDBACK_WIDGET_SOURCE,
...params,
},
hint,
);

logger.debug('Feedback captured with Event ID:', eventId);

return new Promise<string>((resolve, reject) => {
const timeout = setTimeout(() => {
logger.error('Feedback submission timed out.');
reject('Unable to determine if Feedback was correctly sent.');
}, FEEDBACK_RESPONSE_TIMEOUT);

const cleanup = client.on('afterSendEvent', (event: Event, response: TransportMakeRequestResponse) => {
if (event.event_id !== eventId) {
return;
}

clearTimeout(timeout);
cleanup();

if (event.type === 'feedback') {
logger.debug('Feedback detected as sent with eventId:', eventId);
resolve(eventId);
} else {
logger.error('Unexpected response status:', response?.statusCode);
reject('Unable to send Feedback due to unexpected issues.');
}
});
});
};
12 changes: 7 additions & 5 deletions packages/core/test/feedback/FeedbackForm.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { captureFeedback } from '@sentry/core';
import { fireEvent, render, waitFor } from '@testing-library/react-native';
import * as React from 'react';
import { Alert } from 'react-native';

import { FeedbackForm } from '../../src/js/feedback/FeedbackForm';
import type { FeedbackFormProps } from '../../src/js/feedback/FeedbackForm.types';
import { sendFeedback } from '../../src/js/feedback/sendFeedback';
import { checkInternetConnection } from '../../src/js/feedback/utils';

const mockOnFormClose = jest.fn();
Expand All @@ -20,7 +20,6 @@ jest.spyOn(Alert, 'alert');

jest.mock('@sentry/core', () => ({
...jest.requireActual('@sentry/core'),
captureFeedback: jest.fn(),
getCurrentScope: jest.fn(() => ({
getUser: mockGetUser,
})),
Expand All @@ -30,6 +29,9 @@ jest.mock('../../src/js/feedback/utils', () => ({
...jest.requireActual('../../src/js/feedback/utils'),
checkInternetConnection: jest.fn(),
}));
jest.mock('../../src/js/feedback/sendFeedback', () => ({
sendFeedback: jest.fn(),
}));

const defaultProps: FeedbackFormProps = {
onFormClose: mockOnFormClose,
Expand Down Expand Up @@ -131,7 +133,7 @@ describe('FeedbackForm', () => {
});
});

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

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

await waitFor(() => {
expect(captureFeedback).toHaveBeenCalledWith({
expect(sendFeedback).toHaveBeenCalledWith({
message: 'This is a feedback message.',
name: 'John Doe',
email: '[email protected]',
});
}, undefined);
});
});

Expand Down
Loading