Skip to content

feat(feedback): Feedback Widget Drop 2 #4726

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 26 commits into from
May 20, 2025
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1bb4e34
feat(feedback): Report a Bug button (#4378)
antonis Apr 7, 2025
cd6bf1f
Merge branch 'main' into feedback-ui-2
antonis Apr 7, 2025
9a0ab61
feat(feedback): Theming (#4677)
antonis Apr 14, 2025
4c988a8
feat(feedback): Screenshot button (#4714)
antonis Apr 14, 2025
7f8e673
ref(feedback): Extracts FeedbackWidgetProvider in a separate file (#4…
antonis Apr 14, 2025
ad7d3e3
fix(feedback): Fixes accessibility issue on iOS (#4739)
antonis Apr 14, 2025
e7ce2ce
Merge branch 'main' into feedback-ui-2
antonis Apr 14, 2025
3cae215
Merge branch 'main' into feedback-ui-2
antonis Apr 16, 2025
4610d7f
Merge branch 'main' into feedback-ui-2
antonis Apr 16, 2025
bd5bd30
feat(feedback): Screenshot button error flow (#4757)
antonis Apr 17, 2025
b07bf20
Merge branch 'main' into feedback-ui-2
antonis Apr 24, 2025
98f9b1d
Merge branch 'main' into feedback-ui-2
antonis Apr 25, 2025
68bafa1
Increase iOS binary size diff by 100KB (#4784)
antonis Apr 28, 2025
69fc805
Merge branch 'main' into feedback-ui-2
antonis Apr 29, 2025
14dec8c
Merge branch 'main' into feedback-ui-2
antonis May 1, 2025
6cf7905
Fix changelog
antonis May 1, 2025
4492611
Update changelog
antonis May 1, 2025
7d5aba8
Merge branch 'main' into feedback-ui-2
antonis May 7, 2025
f7675d9
Merge branch 'main' into feedback-ui-2
antonis May 8, 2025
92c4d2d
test(e2e): Adds Feedback Widget Maestro E2E tests (#4604)
antonis May 9, 2025
1728356
Merge branch 'main' into feedback-ui-2
antonis May 9, 2025
83c070c
Merge branch 'main' into feedback-ui-2
lucas-zimerman May 13, 2025
469134d
Merge branch 'main' into feedback-ui-2
antonis May 15, 2025
e7a016f
Merge branch 'main' into feedback-ui-2
antonis May 15, 2025
776747f
Merge branch 'main' into feedback-ui-2
antonis May 20, 2025
5c627e6
Merge branch 'main' into feedback-ui-2
antonis May 20, 2025
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -8,10 +8,17 @@

## Unreleased

### Features

- Adds the `FeedbackButton` component that shows the Feedback Widget ([#4378](https://github.com/getsentry/sentry-react-native/pull/4378))
- Adds the `ScreenshotButton` component that takes a screenshot ([#4714](https://github.com/getsentry/sentry-react-native/issues/4714))
- Add Feedback Widget theming ([#4677](https://github.com/getsentry/sentry-react-native/pull/4677))

### Fixes

- crashedLastRun now returns the correct value ([#4829](https://github.com/getsentry/sentry-react-native/pull/4829))
- Use engine-specific promise rejection tracking ([#4826](https://github.com/getsentry/sentry-react-native/pull/4826))
- Fixes Feedback Widget accessibility issue on iOS ([#4739](https://github.com/getsentry/sentry-react-native/pull/4739))

## 6.14.0

19 changes: 19 additions & 0 deletions dev-packages/e2e-tests/maestro/feedback.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
appId: ${APP_ID}
jsEngine: graaljs
---
- runFlow: utils/launchTestAppClear.yml


# The following tests are happy path tests for the feedback widget on both iOS and Android.
# They verify that the feedback form can be opened, filled out, and submitted successfully.
# The tests are separate because iOS tests work better with `testID` and Android tests work better with `text`.

- runFlow:
file: feedback/happyFlow-ios.yml
when:
platform: iOS

- runFlow:
file: feedback/happyFlow-android.yml
when:
platform: Android
39 changes: 39 additions & 0 deletions dev-packages/e2e-tests/maestro/feedback/happyFlow-android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# This is a happy path test for the feedback widget on Android.
# It verifies that the feedback form can be opened, filled out, and submitted successfully
appId: ${APP_ID}
jsEngine: graaljs
---

# Show feedback button
- tapOn: 'Feedback'

# Open feedback widget
- tapOn: 'Report a Bug'

# Assert that the feedback form is visible
- extendedWaitUntil:
visible: 'Report a Bug'
timeout: 5_000

# Fill out name field
- tapOn: 'Your Name'
- inputText: 'John Doe'

# Fill out email field
- tapOn: 'your.email@example.org'
- inputText: 'test@email.com'

# Fill out message field
- tapOn: "What's the bug? What did you expect?"
- inputText: 'This is a test feedback message from CI e2e tests'

# Submit feedback
- scrollUntilVisible:
element:
text: 'Send Bug Report'
- tapOn: 'Send Bug Report'
- assertVisible: 'Thank you for your report!'
- tapOn: 'OK'

# Verify feedback form is closed and the home screen is visible
- assertVisible: 'Welcome to React Native'
45 changes: 45 additions & 0 deletions dev-packages/e2e-tests/maestro/feedback/happyFlow-ios.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# This is a happy path test for the feedback widget on iOS.
# It verifies that the feedback form can be opened, filled out, and submitted successfully
appId: ${APP_ID}
jsEngine: graaljs
---

# Show feedback button
- tapOn: 'Feedback'

# Open feedback widget
- tapOn:
id: 'sentry-feedback-button'

# Assert that the feedback form is visible
- extendedWaitUntil:
visible:
id: 'sentry-feedback-form-title'
timeout: 5_000

# Fill out name field
- tapOn:
id: 'sentry-feedback-name-input'
- inputText: 'John Doe'

# Fill out email field
- tapOn:
id: 'sentry-feedback-email-input'
- inputText: 'test@email.com'

# Fill out message field
- tapOn:
id: 'sentry-feedback-message-input'
- inputText: 'This is a test feedback message from CI e2e tests'

# Submit feedback
- scrollUntilVisible:
element:
id: 'sentry-feedback-submit-button'
- tapOn:
id: 'sentry-feedback-submit-button'
- assertVisible: 'Thank you for your report!'
- tapOn: 'OK'

# Verify feedback form is closed and the home screen is visible
- assertVisible: 'Welcome to React Native'
4 changes: 3 additions & 1 deletion dev-packages/e2e-tests/patch-scripts/rn.patch.app.js
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@ const e2eComponentPatch = '<EndToEndTestsScreen />';
const lastImportRex = /^([^]*)(import\s+[^;]*?;$)/m;
const patchRex = '@sentry/react-native';
const headerComponentRex = /<ScrollView/gm;
const exportDefaultRex = /export\s+default\s+App;/m;

const jsPath = path.join(args.app, 'App.js');
const tsxPath = path.join(args.app, 'App.tsx');
@@ -50,7 +51,8 @@ const isPatched = app.match(patchRex);
if (!isPatched) {
const patched = app
.replace(lastImportRex, m => m + initPatch)
.replace(headerComponentRex, m => e2eComponentPatch + m);
.replace(headerComponentRex, m => e2eComponentPatch + m)
.replace(exportDefaultRex, 'export default Sentry.wrap(App);');

fs.writeFileSync(appPath, patched);
logger.info('Patched RN App.(js|tsx) successfully!');
5 changes: 5 additions & 0 deletions dev-packages/e2e-tests/src/EndToEndTests.tsx
Original file line number Diff line number Diff line change
@@ -62,6 +62,11 @@ const EndToEndTestsScreen = (): JSX.Element => {
name: 'Unhandled Promise Rejection',
action: async () => await Promise.reject(new Error('Unhandled Promise Rejection')),
},
{
id: 'feedback',
name: 'Feedback',
action: () => Sentry.showFeedbackButton(),
},
{
id: 'close',
name: 'Close',
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.ReadableType;
@@ -1038,6 +1039,15 @@ public void getDataFromUri(String uri, Promise promise) {
}
}

public void encodeToBase64(ReadableArray array, Promise promise) {
byte[] bytes = new byte[array.size()];
for (int i = 0; i < array.size(); i++) {
bytes[i] = (byte) array.getInt(i);
}
String base64String = android.util.Base64.encodeToString(bytes, android.util.Base64.DEFAULT);
promise.resolve(base64String);
}

public void crashedLastRun(Promise promise) {
promise.resolve(Sentry.isCrashedLastRun());
}
Original file line number Diff line number Diff line change
@@ -183,6 +183,11 @@ public void getDataFromUri(String uri, Promise promise) {
this.impl.getDataFromUri(uri, promise);
}

@Override
public void encodeToBase64(ReadableArray array, Promise promise) {
this.impl.encodeToBase64(array, promise);
}

@Override
public void popTimeToDisplayFor(String key, Promise promise) {
this.impl.popTimeToDisplayFor(key, promise);
Original file line number Diff line number Diff line change
@@ -183,6 +183,11 @@ public void getDataFromUri(String uri, Promise promise) {
this.impl.getDataFromUri(uri, promise);
}

@ReactMethod
public void encodeToBase64(ReadableArray array, Promise promise) {
this.impl.encodeToBase64(array, promise);
}

@ReactMethod
public void popTimeToDisplayFor(String key, Promise promise) {
this.impl.popTimeToDisplayFor(key, promise);
24 changes: 24 additions & 0 deletions packages/core/ios/RNSentry.mm
Original file line number Diff line number Diff line change
@@ -970,4 +970,28 @@ + (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys
return @YES; // The return ensures that the method is synchronous
}

RCT_EXPORT_METHOD(encodeToBase64
: (NSArray *)array resolver
: (RCTPromiseResolveBlock)resolve rejecter
: (RCTPromiseRejectBlock)reject)
{
NSUInteger count = array.count;
uint8_t *bytes = (uint8_t *)malloc(count);

if (!bytes) {
reject(@"encodeToBase64", @"Memory allocation failed", nil);
return;
}

for (NSUInteger i = 0; i < count; i++) {
bytes[i] = (uint8_t)[array[i] unsignedCharValue];
}

NSData *data = [NSData dataWithBytes:bytes length:count];
free(bytes);

NSString *base64String = [data base64EncodedStringWithOptions:0];
resolve(base64String);
}

@end
1 change: 1 addition & 0 deletions packages/core/src/js/NativeRNSentry.ts
Original file line number Diff line number Diff line change
@@ -51,6 +51,7 @@ export interface Spec extends TurboModule {
getDataFromUri(uri: string): Promise<number[]>;
popTimeToDisplayFor(key: string): Promise<number | undefined | null>;
setActiveSpanId(spanId: string): boolean;
encodeToBase64(data: number[]): Promise<string | undefined | null>;
}

export type NativeStackFrame = {
66 changes: 66 additions & 0 deletions packages/core/src/js/feedback/FeedbackButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as React from 'react';
import type { NativeEventSubscription} from 'react-native';
import { Appearance, Image, Text, TouchableOpacity } from 'react-native';

import { defaultButtonConfiguration } from './defaults';
import { defaultButtonStyles } from './FeedbackWidget.styles';
import { getTheme } from './FeedbackWidget.theme';
import type { FeedbackButtonProps, FeedbackButtonStyles, FeedbackButtonTextConfiguration } from './FeedbackWidget.types';
import { showFeedbackWidget } from './FeedbackWidgetManager';
import { feedbackIcon } from './icons';
import { lazyLoadFeedbackIntegration } from './lazy';

/**
* @beta
* Implements a feedback button that opens the FeedbackForm.
*/
export class FeedbackButton extends React.Component<FeedbackButtonProps> {
private _themeListener: NativeEventSubscription;

public constructor(props: FeedbackButtonProps) {
super(props);
lazyLoadFeedbackIntegration();
}

/**
* Adds a listener for theme changes.
*/
public componentDidMount(): void {
this._themeListener = Appearance.addChangeListener(() => {
this.forceUpdate();
});
}

/**
* Removes the theme listener.
*/
public componentWillUnmount(): void {
if (this._themeListener) {
this._themeListener.remove();
}
}

/**
* Renders the feedback button.
*/
public render(): React.ReactNode {
const theme = getTheme();
const text: FeedbackButtonTextConfiguration = { ...defaultButtonConfiguration, ...this.props };
const styles: FeedbackButtonStyles = {
triggerButton: { ...defaultButtonStyles(theme).triggerButton, ...this.props.styles?.triggerButton },
triggerText: { ...defaultButtonStyles(theme).triggerText, ...this.props.styles?.triggerText },
triggerIcon: { ...defaultButtonStyles(theme).triggerIcon, ...this.props.styles?.triggerIcon },
};

return (
<TouchableOpacity
style={styles.triggerButton}
onPress={showFeedbackWidget}
accessibilityLabel={text.triggerAriaLabel}
>
<Image source={{ uri: feedbackIcon }} style={styles.triggerIcon}/>
<Text style={styles.triggerText} testID='sentry-feedback-button'>{text.triggerLabel}</Text>
</TouchableOpacity>
);
}
}
Loading