Skip to content

Commit

Permalink
Merge 9ea5496 into 8ab11b6
Browse files Browse the repository at this point in the history
  • Loading branch information
antonis authored Nov 28, 2024
2 parents 8ab11b6 + 9ea5496 commit b2f7f82
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 61 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@
## 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";

const eventId = Sentry.lastEventId();

Sentry.captureFeedback({
name: "John Doe",
email: "[email protected]",
message: "Hello World!",
associatedEventId: eventId, // optional
});
```

### 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, {
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);
}

/**
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);
}

/**
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
19 changes: 18 additions & 1 deletion samples/react-native-macos/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
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

0 comments on commit b2f7f82

Please sign in to comment.