Skip to content

chore(feedback): Update the samples with an attachment/tags example #4322

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

Merged
merged 25 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
817eac8
Update the client implementation to use the new capture feedback js api
antonis Nov 27, 2024
5370a99
Updates SDK API
antonis Nov 27, 2024
42e2fa1
Adds new feedback button in the sample
antonis Nov 27, 2024
514b102
Adds changelog
antonis Nov 27, 2024
0935bbd
Removes unused mock
antonis Nov 27, 2024
fc22384
Adds event hint
antonis Nov 27, 2024
8b55e8a
Updates samples
antonis Nov 27, 2024
cd178f4
Updates changelog
antonis Nov 27, 2024
8a7018e
Fix lint issue
antonis Nov 27, 2024
fa41526
Updates changelog
antonis Nov 27, 2024
9ea5496
Update CHANGELOG.md
antonis Nov 28, 2024
da0d4ac
Directly use captureFeedback from sentry/core
antonis Nov 28, 2024
3e36c6d
Use import from core
antonis Nov 28, 2024
5f3df64
Fixes imports order lint issue
antonis Nov 28, 2024
71b28e8
Fixes build issue
antonis Nov 28, 2024
f9d2b59
Adds captureFeedback tests from sentry-javascript
antonis Nov 28, 2024
0e54497
Merge branch 'antonis/3859-newCaptureFeedbackAPI' into antonis/3859-n…
antonis Nov 28, 2024
8428d38
Removes unused import
antonis Nov 28, 2024
bd027d8
Removes unneeded PR from changelog
antonis Nov 28, 2024
d05d531
Update CHANGELOG.md
krystofwoldrich Nov 28, 2024
2bb104b
Only deprecate client captureUserFeedback
antonis Nov 28, 2024
dd4be93
Merge branch 'antonis/3859-newCaptureFeedbackAPI' into antonis/3859-n…
antonis Nov 28, 2024
3ec17bd
Upload a simple textfile
antonis Nov 28, 2024
70ce204
Merge branch 'main' into antonis/3859-newCaptureFeedbackAPI-hint
antonis Dec 2, 2024
a3a83bd
Update CHANGELOG.md
krystofwoldrich Dec 2, 2024
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
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,36 @@

## Unreleased

### Features

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

```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, {
captureContext: {
tags: { 'tag-key': 'tag-value' },
},
attachments: [
{
filename: 'screenshot.png',
data: 'base64-encoded-image',
},
],
});
```

### 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, hint?: EventHint): void {
captureFeedbackApi(feedback, hint);
}

/**
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, EventHint, 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, hint?: EventHint): void {
getClient<ReactNativeClient>()?.captureFeedback(feedbackParams, hint);
}

/**
Expand Down
92 changes: 58 additions & 34 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,58 @@ 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',
};

client.captureFeedback(feedback);

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

test('sends UserFeedback with hint', () => {
const client = new ReactNativeClient({
...DEFAULT_OPTIONS,
dsn: EXAMPLE_DSN,
});
jest.mock('@sentry/browser', () => ({
captureFeedback: jest.fn(),
}));

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',
const feedback: SendFeedbackParams = {
message: 'Test Comments',
email: '[email protected]',
name: 'Test User',
event_id: 'testEvent123',
});
associatedEventId: 'testEvent123',
};

const hint = {
captureContext: {
tags: { testtag: 'testvalue' },
},
attachments: [
{
filename: 'screenshot.png',
data: 'base64Image',
},
],
};

client.captureFeedback(feedback, hint);

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

Expand Down Expand Up @@ -417,11 +446,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,53 @@ 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="Send feedback with attachment and tags"
color="#6C5FC7"
onPress={async () => {
onDismiss();

const base64Image =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wcAAgUBA+kZYq8AAAAASUVORK5CYII=';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just an one pixel image. I avoided to add extra library dependencies to handle a real image attachment at this point for simplicity. This sample will be enhanced along with the capture feedback UI implementation #4302

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For simplicity avoiding extra lib is okay. I'm not sure if this will get correctly recognized in Sentry.

The data type is UInt8Array | string, but when string is used it's expected the string is the raw data.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's check if the image is correctly processed if not, we can just bake in here the UInt8Array.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is not working - the file attached on sentry is just a text file with base64

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the feedback @radiodario 🙇

the file attached on sentry is just a text file with base64

That is true. We currently support only raw data attachments (updated code, User Feedback API documentation). In order to upload an image you need to pass a Uint8Array in the data field.
Alternatively you may Integrate with an Image Picker Library or Implementing the onAddScreenshot Callback.


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

Sentry.captureFeedback(userFeedback, {
captureContext: {
tags: { testtag: 'testvalue' },
},
attachments: [
{
filename: 'screenshot.png',
data: base64Image,
},
],
});
clearComments();
}}
/>
<View style={styles.buttonSpacer} />
<Button
title="Close"
color="#6C5FC7"
Expand Down
Loading
Loading