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

feat: Add new captureFeedback API to RN SDK #4320

Merged
merged 13 commits into from
Dec 2, 2024
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,26 @@

## Unreleased

### Features

- Adds new `captureFeedback` and deprecates the `captureUserFeedback` API ([#4320](https://github.com/getsentry/sentry-react-native/pull/4320))

```jsx
import * as Sentry from "@sentry/react-native";
import { SendFeedbackParams } from "@sentry/react-native";

const eventId = Sentry.captureMessage("My Message");
// OR: const eventId = Sentry.lastEventId();

const userFeedback: SendFeedbackParams = {
name: "John Doe",
email: "[email protected]",
message: "Hello World!",
associatedEventId: eventId,// Optional
};
Sentry.captureFeedback(userFeedback);
```
antonis marked this conversation as resolved.
Show resolved Hide resolved

### Fixes

- Return `lastEventId` export from `@sentry/core` ([#4315](https://github.com/getsentry/sentry-react-native/pull/4315))
Expand Down
16 changes: 5 additions & 11 deletions packages/core/src/js/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { eventFromException, eventFromMessage } from '@sentry/browser';
import { captureFeedback as captureFeedbackApi, eventFromException, eventFromMessage } from '@sentry/browser';
import { BaseClient } from '@sentry/core';
import type {
ClientReportEnvelope,
Expand All @@ -7,9 +7,9 @@ import type {
Event,
EventHint,
Outcome,
SendFeedbackParams,
SeverityLevel,
TransportMakeRequestResponse,
UserFeedback,
} from '@sentry/types';
import { dateTimestampInSeconds, logger, SentryError } from '@sentry/utils';
import { Alert } from 'react-native';
Expand All @@ -20,7 +20,7 @@ import { getDefaultSidecarUrl } from './integrations/spotlight';
import type { ReactNativeClientOptions } from './options';
import type { mobileReplayIntegration } from './replay/mobilereplay';
import { MOBILE_REPLAY_INTEGRATION_NAME } from './replay/mobilereplay';
import { createUserFeedbackEnvelope, items } from './utils/envelope';
import { items } from './utils/envelope';
import { ignoreRequireCycleLogs } from './utils/ignorerequirecyclelogs';
import { mergeOutcomes } from './utils/outcome';
import { ReactNativeLibraries } from './utils/rnlibraries';
Expand Down Expand Up @@ -86,14 +86,8 @@ export class ReactNativeClient extends BaseClient<ReactNativeClientOptions> {
/**
* Sends user feedback to Sentry.
*/
public captureUserFeedback(feedback: UserFeedback): void {
const envelope = createUserFeedbackEnvelope(feedback, {
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
metadata: this._options._metadata,
dsn: this.getDsn(),
tunnel: undefined,
});
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendEnvelope(envelope);
public captureFeedback(feedback: SendFeedbackParams): void {
captureFeedbackApi(feedback);
}
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved

/**
Expand Down
13 changes: 12 additions & 1 deletion packages/core/src/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type {
SdkInfo,
Event,
Exception,
SendFeedbackParams,
SeverityLevel,
StackFrame,
Stacktrace,
Expand Down Expand Up @@ -59,7 +60,17 @@ export { SDK_NAME, SDK_VERSION } from './version';
export type { ReactNativeOptions } from './options';
export { ReactNativeClient } from './client';

export { init, wrap, nativeCrash, flush, close, captureUserFeedback, withScope, crashedLastRun } from './sdk';
export {
init,
wrap,
nativeCrash,
flush,
close,
captureFeedback,
captureUserFeedback,
withScope,
crashedLastRun,
} from './sdk';
export { TouchEventBoundary, withTouchEventBoundary } from './touchevents';

export {
Expand Down
18 changes: 16 additions & 2 deletions packages/core/src/js/sdk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
defaultStackParser,
makeFetchTransport,
} from '@sentry/react';
import type { Breadcrumb, BreadcrumbHint, Integration, Scope, UserFeedback } from '@sentry/types';
import type { Breadcrumb, BreadcrumbHint, Integration, Scope, SendFeedbackParams, UserFeedback } from '@sentry/types';
import { logger, stackParserFromStackParserOptions } from '@sentry/utils';
import * as React from 'react';

Expand Down Expand Up @@ -219,9 +219,23 @@ export async function close(): Promise<void> {

/**
* Captures user feedback and sends it to Sentry.
* @deprecated Use `Sentry.captureFeedback` instead.
*/
export function captureUserFeedback(feedback: UserFeedback): void {
getClient<ReactNativeClient>()?.captureUserFeedback(feedback);
const feedbackParams = {
name: feedback.name,
email: feedback.email,
message: feedback.comments,
associatedEventId: feedback.event_id,
};
captureFeedback(feedbackParams);
}

/**
* Captures user feedback and sends it to Sentry.
*/
export function captureFeedback(feedbackParams: SendFeedbackParams): void {
getClient<ReactNativeClient>()?.captureFeedback(feedbackParams);
}
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved

/**
Expand Down
65 changes: 28 additions & 37 deletions packages/core/test/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import * as mockedtimetodisplaynative from './tracing/mockedtimetodisplaynative';
jest.mock('../src/js/tracing/timetodisplaynative', () => mockedtimetodisplaynative);

import { defaultStackParser } from '@sentry/browser';
import type { Envelope, Event, Outcome, Transport, TransportMakeRequestResponse } from '@sentry/types';
import { captureFeedback as captureFeedbackApi, defaultStackParser } from '@sentry/browser';
import type {
Envelope,
Event,
Outcome,
SendFeedbackParams,
Transport,
TransportMakeRequestResponse,
} from '@sentry/types';
import { rejectedSyncPromise, SentryError } from '@sentry/utils';
import * as RN from 'react-native';

Expand All @@ -19,7 +26,6 @@ import {
envelopeItems,
firstArg,
getMockSession,
getMockUserFeedback,
getSyncPromiseRejectOnFirstCall,
} from './testutils';

Expand Down Expand Up @@ -76,6 +82,14 @@ jest.mock(
}),
);

jest.mock('@sentry/browser', () => {
const actual = jest.requireActual('@sentry/browser');
return {
...actual,
captureFeedback: jest.fn(),
};
});

const EXAMPLE_DSN = 'https://[email protected]/148053';

const DEFAULT_OPTIONS: ReactNativeClientOptions = {
Expand Down Expand Up @@ -187,15 +201,6 @@ describe('Tests ReactNativeClient', () => {
expect(mockTransport.send).not.toBeCalled();
});

test('captureUserFeedback does not call transport when enabled false', () => {
const mockTransport = createMockTransport();
const client = createDisabledClientWith(mockTransport);

client.captureUserFeedback(getMockUserFeedback());

expect(mockTransport.send).not.toBeCalled();
});

function createDisabledClientWith(transport: Transport) {
return new ReactNativeClient({
...DEFAULT_OPTIONS,
Expand Down Expand Up @@ -290,34 +295,25 @@ describe('Tests ReactNativeClient', () => {
});

describe('UserFeedback', () => {
test('sends UserFeedback to native Layer', () => {
const mockTransportSend: jest.Mock = jest.fn(() => Promise.resolve());
test('sends UserFeedback', () => {
const client = new ReactNativeClient({
...DEFAULT_OPTIONS,
dsn: EXAMPLE_DSN,
transport: () => ({
send: mockTransportSend,
flush: jest.fn(),
}),
});
jest.mock('@sentry/browser', () => ({
captureFeedback: jest.fn(),
}));

client.captureUserFeedback({
comments: 'Test Comments',
const feedback: SendFeedbackParams = {
message: 'Test Comments',
email: '[email protected]',
name: 'Test User',
event_id: 'testEvent123',
});
associatedEventId: 'testEvent123',
};

expect(mockTransportSend.mock.calls[0][firstArg][envelopeHeader].event_id).toEqual('testEvent123');
expect(mockTransportSend.mock.calls[0][firstArg][envelopeItems][0][envelopeItemHeader].type).toEqual(
'user_report',
);
expect(mockTransportSend.mock.calls[0][firstArg][envelopeItems][0][envelopeItemPayload]).toEqual({
comments: 'Test Comments',
email: '[email protected]',
name: 'Test User',
event_id: 'testEvent123',
});
client.captureFeedback(feedback);

expect(captureFeedbackApi).toHaveBeenCalledWith(feedback);
});
});

Expand Down Expand Up @@ -417,11 +413,6 @@ describe('Tests ReactNativeClient', () => {
client.captureSession(getMockSession());
expect(getSdkInfoFrom(mockTransportSend)).toStrictEqual(expectedSdkInfo);
});

test('send SdkInfo in the user feedback envelope header', () => {
client.captureUserFeedback(getMockUserFeedback());
expect(getSdkInfoFrom(mockTransportSend)).toStrictEqual(expectedSdkInfo);
});
});

describe('event data enhancement', () => {
Expand Down
9 changes: 1 addition & 8 deletions packages/core/test/testutils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Session, Transport, UserFeedback } from '@sentry/types';
import type { Session, Transport } from '@sentry/types';
import { rejectedSyncPromise } from '@sentry/utils';

export type MockInterface<T> = {
Expand Down Expand Up @@ -36,13 +36,6 @@ export const getMockSession = (): Session => ({
}),
});

export const getMockUserFeedback = (): UserFeedback => ({
comments: 'comments_test_value',
email: 'email_test_value',
name: 'name_test_value',
event_id: 'event_id_test_value',
});

export const getSyncPromiseRejectOnFirstCall = <Y extends any[]>(reason: unknown): jest.Mock => {
let shouldSyncReject = true;
return jest.fn((..._args: Y) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { View, StyleSheet, Text, TextInput, Image, Button } from 'react-native';
import * as Sentry from '@sentry/react-native';
import { UserFeedback } from '@sentry/react-native';
import { SendFeedbackParams, UserFeedback } from '@sentry/react-native';

export const DEFAULT_COMMENTS = "It's broken again! Please fix it.";

Expand Down Expand Up @@ -48,6 +48,23 @@ export function UserFeedbackModal(props: { onDismiss: () => void }) {
}}
/>
<View style={styles.buttonSpacer} />
<Button
title="Send feedback without event"
color="#6C5FC7"
onPress={async () => {
onDismiss();

const userFeedback: SendFeedbackParams = {
message: comments,
name: 'John Doe',
email: '[email protected]',
};

Sentry.captureFeedback(userFeedback);
clearComments();
}}
/>
<View style={styles.buttonSpacer} />
<Button
title="Close"
color="#6C5FC7"
Expand Down
19 changes: 18 additions & 1 deletion samples/react-native/src/components/UserFeedbackModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { View, StyleSheet, Text, TextInput, Image, Button } from 'react-native';
import * as Sentry from '@sentry/react-native';
import { UserFeedback } from '@sentry/react-native';
import { SendFeedbackParams, UserFeedback } from '@sentry/react-native';

export const DEFAULT_COMMENTS = "It's broken again! Please fix it.";

Expand Down Expand Up @@ -48,6 +48,23 @@ export function UserFeedbackModal(props: { onDismiss: () => void }) {
}}
/>
<View style={styles.buttonSpacer} />
<Button
title="Send feedback without event"
color="#6C5FC7"
onPress={async () => {
onDismiss();

const userFeedback: SendFeedbackParams = {
message: comments,
name: 'John Doe',
email: '[email protected]',
};

Sentry.captureFeedback(userFeedback);
clearComments();
}}
/>
<View style={styles.buttonSpacer} />
<Button
title="Close"
color="#6C5FC7"
Expand Down
Loading