Skip to content

Commit 118f4f4

Browse files
feat: send common errors automatically, instead of asking user to review (#1799)
- Closes #1678 - Closes FE-1019 ## Summary - We now detect and handle common errors either by sending them directly or ignoring them. # Checklist - [x] I've added error handling for all actions/requests. - [x] I've reviewed how my changes will display in the UI. - [x] I've checked all the copy (text) changes or additions in this PR. - [x] I've included references to the issues being closed on GitHub and/or Linear. - [x] I've ensured that the documentation is up to date and reflects any changes. - [x] I've added documentation links where it may be helpful. - [x] I **reviewed** the **entire PR** myself. --------- Co-authored-by: LuizAsFight <felipebolsonigomes@gmail.com> Co-authored-by: Luiz Gomes <8636507+LuizAsFight@users.noreply.github.com>
1 parent 20042d8 commit 118f4f4

File tree

10 files changed

+317
-237
lines changed

10 files changed

+317
-237
lines changed

.changeset/selfish-days-repeat.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"fuels-wallet": patch
3+
---
4+
5+
feat: send common errors automatically, instead of asking user to review

examples/cra-dapp/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@
2020
"@types/react-dom": "18.3.0",
2121
"@vitejs/plugin-react": "4.2.1",
2222
"typescript": "5.2.2",
23-
"vite": "6.0.3"
23+
"vite": "6.0.8"
2424
}
2525
}

packages/app/playwright/e2e/ReportError.test.ts

+55-18
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ test.describe('ReportError', () => {
4848
await getByAriaLabel(page, 'Send error reports').click();
4949
await expect(page.getByText(/Unexpected error/)).toHaveCount(0);
5050

51-
await page.waitForTimeout(2000);
52-
const errorsAfterReporting = await getPageErrors(page);
53-
expect(errorsAfterReporting.length).toBe(0);
51+
await expect
52+
.poll(async () => (await getPageErrors(page)).length, {
53+
timeout: 10000,
54+
})
55+
.toBe(0);
5456
});
5557

5658
test('should show Review Error in menu when there is a error in the database', async () => {
@@ -59,7 +61,7 @@ test.describe('ReportError', () => {
5961
await window.fuelDB.errors.add({
6062
id: '12345',
6163
error: {
62-
name: 'React error',
64+
name: 'React$ error',
6365
message: 'Test Error',
6466
stack: 'Line error 1',
6567
},
@@ -85,8 +87,8 @@ test.describe('ReportError', () => {
8587
await window.fuelDB.errors.add({
8688
id: '12345',
8789
error: {
88-
name: 'React error',
89-
message: 'Test Error',
90+
name: 'React$ error',
91+
message: 'Test$ Error',
9092
stack: 'Line error 1',
9193
},
9294
extra: {
@@ -103,20 +105,24 @@ test.describe('ReportError', () => {
103105
page.locator(`[data-key="hasErrors"]`).click();
104106
await hasText(page, /Unexpected error/i);
105107

108+
expect((await getPageErrors(page)).length).toBe(1);
106109
// report error
107110
await getByAriaLabel(page, 'Ignore error(s)').click();
108111
await expect(page.getByText(/Unexpected error/i)).toHaveCount(0);
109112

110-
const errorsAfterReporting = await getPageErrors(page);
111-
expect(errorsAfterReporting.length).toBe(1);
113+
await expect
114+
.poll(async () => (await getPageErrors(page)).length, {
115+
timeout: 10000,
116+
})
117+
.toBe(1);
112118
});
113119
test('should be able to dismiss all errors', async () => {
114120
await visit(page, '/');
115121
await page.evaluate(async () => {
116122
await window.fuelDB.errors.add({
117123
id: '12345',
118124
error: {
119-
name: 'React error',
125+
name: 'React$ error',
120126
message: 'Test Error',
121127
stack: 'Line error 1',
122128
},
@@ -141,16 +147,19 @@ test.describe('ReportError', () => {
141147
).click();
142148
await expect(page.getByText(/Unexpected error/i)).toHaveCount(0);
143149

144-
const errorsAfterReporting = await getPageErrors(page);
145-
expect(errorsAfterReporting.length).toBe(0);
150+
await expect
151+
.poll(async () => (await getPageErrors(page)).length, {
152+
timeout: 10000,
153+
})
154+
.toBe(0);
146155
});
147156
test('should hide when the single error is dismissed', async () => {
148157
await visit(page, '/');
149158
await page.evaluate(async () => {
150159
await window.fuelDB.errors.add({
151160
id: '12345',
152161
error: {
153-
name: 'React error',
162+
name: 'React$ error',
154163
message: 'Test Error',
155164
stack: 'Line error 1',
156165
},
@@ -172,14 +181,17 @@ test.describe('ReportError', () => {
172181
await getByAriaLabel(page, 'Dismiss error').click();
173182
await expect(page.getByText(/Unexpected error/i)).toHaveCount(0);
174183

175-
const errorsAfterReporting = await getPageErrors(page);
176-
expect(errorsAfterReporting.length).toBe(0);
184+
await expect
185+
.poll(async () => (await getPageErrors(page)).length, {
186+
timeout: 10000,
187+
})
188+
.toBe(0);
177189
});
178190
test('should detect and capture global errors', async () => {
179-
await visit(page, '/');
180191
await page.evaluate(async () => {
181-
console.error(new Error('Test Error'));
192+
console.error(new Error('New Error'));
182193
});
194+
await visit(page, '/');
183195
await reload(page);
184196
await getByAriaLabel(page, 'Menu').click();
185197
page.locator(`[data-key="hasErrors"]`).click();
@@ -195,7 +207,7 @@ test.describe('ReportError', () => {
195207
await window.fuelDB.errors.add({
196208
id: '12345',
197209
error: {
198-
name: 'React error',
210+
name: 'React$ error',
199211
message: 'Test Error',
200212
stack: 'Line error 1',
201213
},
@@ -210,7 +222,7 @@ test.describe('ReportError', () => {
210222
await window.fuelDB.errors.add({
211223
id: '123456',
212224
error: {
213-
name: 'React error',
225+
name: 'React$ error',
214226
message: 'Test Error',
215227
stack: 'Line error 1',
216228
},
@@ -231,4 +243,29 @@ test.describe('ReportError', () => {
231243
const errorsAfterReporting = await getPageErrors(page);
232244
expect(errorsAfterReporting.length).toBe(1);
233245
});
246+
test('should not show ignored errors', async () => {
247+
await visit(page, '/');
248+
249+
await page.evaluate(async () => {
250+
await window.fuelDB.errors.add({
251+
id: '12345',
252+
error: {
253+
name: 'React Error',
254+
message: 'React Error',
255+
stack: 'Line error 1',
256+
},
257+
extra: {
258+
timestamp: Date.now(),
259+
location: 'http://localhost:3000',
260+
pathname: '/',
261+
hash: '#',
262+
counts: 0,
263+
},
264+
});
265+
});
266+
await getByAriaLabel(page, 'Menu').click();
267+
expect(
268+
await page.locator(`[data-key="hasErrors"]`).isVisible()
269+
).toBeFalsy();
270+
});
234271
});
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const BALANCE_NFTS_TAB_HEIGHT = 244;
1+
export const BALANCE_NFTS_TAB_HEIGHT = 244;

packages/app/src/systems/Account/components/QuickAccountConnect/QuickAccountConnect.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ export const QuickAccountConnect = () => {
6161
const onDismiss = () => {
6262
if (!origin || !account) return;
6363
setDismissed(true);
64-
localStorage.setItem(getDismissKey(account.address, origin.full), 'true');
64+
if (typeof localStorage !== 'undefined') {
65+
localStorage.setItem(getDismissKey(account.address, origin.full), 'true');
66+
}
6567
};
6668

6769
useEffect(() => {

packages/app/src/systems/Error/machines/reportErrorMachine.tsx

+11-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { assign, createMachine } from 'xstate';
44

55
import { db } from '~/systems/Core/utils/database';
66
import { ErrorProcessorService } from '~/systems/Error/services/ErrorProcessorService';
7+
import { getErrorIgnoreData } from '~/systems/Error/utils/getErrorIgnoreData';
78
import { ReportErrorService } from '../services';
89

910
export type ErrorMachineContext = {
@@ -124,6 +125,9 @@ export const reportErrorMachine = createMachine(
124125
target: 'idle',
125126
},
126127
],
128+
onError: {
129+
target: 'idle',
130+
},
127131
},
128132
},
129133
reporting: {
@@ -175,7 +179,13 @@ export const reportErrorMachine = createMachine(
175179
checkForErrors: async (context) => {
176180
await context.errorProcessorService.processErrors();
177181
const hasErrors = await context.reportErrorService.checkForErrors();
178-
const errors = await context.reportErrorService.getErrors();
182+
const errors = (await context.reportErrorService.getErrors()).filter(
183+
(e) => !getErrorIgnoreData(e?.error)?.action
184+
);
185+
await context.reportErrorService.handleAndRemoveOldIgnoredErrors(
186+
errors
187+
);
188+
179189
return {
180190
hasErrors,
181191
errors,

packages/app/src/systems/Error/services/ReportErrorService.tsx

+48-21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { StoredFuelWalletError } from '@fuel-wallet/types';
2-
import * as Sentry from '@sentry/react';
32
import { db } from '~/systems/Core/utils/database';
3+
import { captureException } from '~/systems/Error/utils/captureException';
4+
import { getErrorIgnoreData } from '~/systems/Error/utils/getErrorIgnoreData';
45
import { parseFuelError } from '../utils';
56

67
export class ReportErrorService {
@@ -12,29 +13,32 @@ export class ReportErrorService {
1213
if (typeof window !== 'undefined' && (window as any).playwright) {
1314
return;
1415
}
15-
Sentry.captureException(e.error, {
16-
extra: e.extra,
17-
tags: { id: e.id, manual: true },
18-
});
16+
captureException(e.error, e.extra);
1917
}
2018
}
2119

2220
static async saveError(error: Error) {
23-
const parsedError = parseFuelError(error);
24-
if (!parsedError) {
25-
console.warn(`Can't save error without a message`);
26-
return;
27-
}
28-
if (!('id' in parsedError)) {
29-
console.warn(`Can't save error without an id`);
30-
return;
31-
}
32-
if (!db.isOpen() || db.hasBeenClosed()) {
33-
console.warn('Error saving error: db is closed');
34-
return;
35-
}
36-
3721
try {
22+
const parsedError = parseFuelError(error);
23+
const ignoreData = getErrorIgnoreData(parsedError?.error);
24+
if (!parsedError) {
25+
console.warn(`Can't save error without a message`);
26+
return;
27+
}
28+
if (!('id' in parsedError)) {
29+
console.warn(`Can't save error without an id`);
30+
return;
31+
}
32+
if (ignoreData?.action === 'ignore') return;
33+
if (ignoreData?.action === 'hide') {
34+
// Directly report to Sentry and exit
35+
captureException(parsedError.error, parsedError.extra);
36+
return;
37+
}
38+
if (!db.isOpen() || db.hasBeenClosed()) {
39+
console.warn('Error saving error: db is closed');
40+
return;
41+
}
3842
return await db.errors.add(parsedError);
3943
} catch (e) {
4044
console.warn('Failed to save error', e);
@@ -43,17 +47,40 @@ export class ReportErrorService {
4347

4448
async checkForErrors(): Promise<boolean> {
4549
const errors = await this.getErrors();
46-
return errors.length > 0;
50+
return (
51+
errors.filter((e) => !getErrorIgnoreData(e?.error)?.action).length > 0
52+
);
4753
}
4854

4955
async getErrors(): Promise<StoredFuelWalletError[]> {
50-
return db.errors.toArray();
56+
return await db.errors.toArray();
5157
}
5258

5359
async clearErrors() {
5460
await db.errors.clear();
5561
}
5662

63+
async handleAndRemoveOldIgnoredErrors(errors: StoredFuelWalletError[]) {
64+
const errorsBeingRemoved: Array<Promise<unknown>> = [];
65+
// Convert to for of
66+
for (const e of errors) {
67+
const errorIgnoreData = getErrorIgnoreData(e?.error);
68+
if (errorIgnoreData?.action) {
69+
errorsBeingRemoved.push(
70+
new Promise((resolve) => {
71+
if (errorIgnoreData?.action === 'hide') {
72+
captureException(e.error, e.extra);
73+
}
74+
return resolve(
75+
this.dismissError(e.id).finally(() => resolve(true))
76+
);
77+
})
78+
);
79+
}
80+
}
81+
await Promise.all(errorsBeingRemoved);
82+
}
83+
5784
async dismissError(key: string) {
5885
if (!key) return;
5986
db.errors.delete(key);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { SentryExtraErrorData } from '@fuel-wallet/types';
2+
import * as Sentry from '@sentry/react';
3+
4+
export function captureException(error: Error, extra: SentryExtraErrorData) {
5+
Sentry.captureException(error, {
6+
extra,
7+
tags: { manual: true },
8+
});
9+
}

0 commit comments

Comments
 (0)