Skip to content

Commit d72154a

Browse files
committed
chore: track suppressed errors
1 parent 9b5e05b commit d72154a

39 files changed

Lines changed: 1132 additions & 62 deletions

packages/snap/snap.manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://github.com/MetaMask/snap-solana-wallet.git"
88
},
99
"source": {
10-
"shasum": "iJ+8qMIGrVGUFKqVoiq321vPMMrzfbh0pJypqrjfYZI=",
10+
"shasum": "tL5sIl2cqcBuEQv2bedRbZGHwX4TH4SiwoJ46XQVt7I=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",

packages/snap/src/core/clients/nft-api/NftApiClient.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ import { InMemoryCache } from '../../caching/InMemoryCache';
44
import type { Serializable } from '../../serialization/types';
55
import type { ConfigProvider } from '../../services/config';
66
import { mockLogger } from '../../services/mocks/logger';
7+
import { trackError } from '../../utils/errors';
78
import { MOCK_NFT_METADATA_RESPONSE_MAPPED } from './mocks/mockNftMetadataResponseMapped';
89
import { MOCK_NFT_METADATA_RESPONSE_RAW } from './mocks/mockNftMetadataResponseRaw';
910
import { MOCK_NFTS_LIST_RESPONSE_MAPPED } from './mocks/mockNftsListResponseMapped';
1011
import { MOCK_NFTS_LIST_RESPONSE_RAW } from './mocks/mockNftsListResponseRaw';
1112
import { NftApiClient } from './NftApiClient';
1213

14+
jest.mock('../../utils/errors', () => ({
15+
trackError: jest.fn().mockResolvedValue('tracked-error-id'),
16+
}));
17+
1318
const mockFetch = jest.fn();
1419
let mockCache: ICache<Serializable>;
1520

@@ -123,9 +128,11 @@ describe('NftApiClient', () => {
123128
});
124129

125130
it('should return null if the fetch fails', async () => {
126-
mockFetch.mockRejectedValueOnce(new Error('some-error'));
131+
const error = new Error('some-error');
132+
mockFetch.mockRejectedValueOnce(error);
127133
const nft = await client.getNftMetadata('some-token-address');
128134
expect(nft).toBeNull();
135+
expect(trackError).toHaveBeenCalledWith(error);
129136
});
130137
});
131138

packages/snap/src/core/clients/nft-api/NftApiClient.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useCache } from '../../caching/useCache';
77
import type { Serializable } from '../../serialization/types';
88
import type { ConfigProvider } from '../../services/config';
99
import { buildUrl } from '../../utils/buildUrl';
10+
import { trackError } from '../../utils/errors';
1011
import type { ILogger } from '../../utils/logger';
1112
import logger from '../../utils/logger';
1213
import { UrlStruct } from '../../validation/structs';
@@ -202,6 +203,7 @@ export class NftApiClient {
202203

203204
return mappedData;
204205
} catch (error) {
206+
await trackError(error);
205207
return null;
206208
}
207209
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { SolMethod } from '@metamask/keyring-api';
2+
3+
import { transactionScanService, state } from '../../../../snapContext';
4+
import { Network } from '../../../constants/solana';
5+
import { serialize } from '../../../serialization/serialize';
6+
import { trackError } from '../../../utils/errors';
7+
import { getInterfaceContext, updateInterface } from '../../../utils/interface';
8+
import { refreshConfirmationEstimation } from './refreshConfirmationEstimation';
9+
10+
jest.mock('../../../utils/errors', () => ({
11+
trackError: jest.fn().mockResolvedValue('tracked-error-id'),
12+
}));
13+
14+
jest.mock('../../../serialization/serialize', () => ({
15+
serialize: jest.fn((value) => value),
16+
}));
17+
18+
jest.mock('../../../utils/interface', () => ({
19+
CONFIRM_SIGN_AND_SEND_TRANSACTION_INTERFACE_NAME: 'confirmation-interface',
20+
getInterfaceContext: jest.fn(),
21+
updateInterface: jest.fn(),
22+
}));
23+
24+
jest.mock(
25+
'../../../../features/confirmation/views/ConfirmTransactionRequest/ConfirmTransactionRequest',
26+
() => ({
27+
ConfirmTransactionRequest: () => null,
28+
}),
29+
);
30+
31+
jest.mock('../../../../snapContext', () => ({
32+
state: {
33+
getKey: jest.fn(),
34+
},
35+
transactionScanService: {
36+
scanTransaction: jest.fn(),
37+
},
38+
}));
39+
40+
const setupTest = () => {
41+
const interfaceContext = {
42+
account: {
43+
address: 'BLw3RweJmfbTapJRgnPRvd962YDjFYAnVGd1p5hmZ5tP',
44+
},
45+
transaction: 'mock-transaction',
46+
scope: Network.Mainnet,
47+
method: SolMethod.SignAndSendTransaction,
48+
origin: 'https://metamask.io',
49+
preferences: {
50+
simulateOnChainActions: true,
51+
},
52+
scanFetchStatus: 'fetched',
53+
};
54+
55+
(state.getKey as jest.Mock).mockResolvedValue({
56+
'confirmation-interface': 'interface-id',
57+
});
58+
(getInterfaceContext as jest.Mock).mockResolvedValue(interfaceContext);
59+
(updateInterface as jest.Mock).mockResolvedValue(undefined);
60+
(serialize as jest.Mock).mockImplementation((value) => value);
61+
};
62+
63+
describe('refreshConfirmationEstimation', () => {
64+
it('tracks refresh failures and restores the fetched state', async () => {
65+
setupTest();
66+
67+
const error = new Error('Scan failed');
68+
69+
(transactionScanService.scanTransaction as jest.Mock).mockRejectedValue(
70+
error,
71+
);
72+
73+
await refreshConfirmationEstimation({ request: {} as any });
74+
75+
expect(trackError).toHaveBeenCalledWith(error);
76+
expect(updateInterface).toHaveBeenLastCalledWith(
77+
'interface-id',
78+
null,
79+
expect.objectContaining({
80+
scanFetchStatus: 'fetched',
81+
}),
82+
);
83+
});
84+
});

packages/snap/src/core/handlers/onCronjob/backgroundEvents/refreshConfirmationEstimation.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { ConfirmTransactionRequestContext } from '../../../../features/conf
55
import { state, transactionScanService } from '../../../../snapContext';
66
import { serialize } from '../../../serialization/serialize';
77
import type { UnencryptedStateValue } from '../../../services/state/State';
8+
import { trackError } from '../../../utils/errors';
89
import {
910
CONFIRM_SIGN_AND_SEND_TRANSACTION_INTERFACE_NAME,
1011
getInterfaceContext,
@@ -125,6 +126,8 @@ export const refreshConfirmationEstimation: OnCronjobHandler = async () => {
125126
},
126127
});
127128
} catch (error) {
129+
await trackError(error);
130+
128131
// Check if context exists in case the UI was closed, nothing to rollback
129132
const fetchedInterfaceContext =
130133
await getInterfaceContext<ConfirmTransactionRequestContext>(
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { assetsService, priceApiClient, state } from '../../../../snapContext';
2+
import { KnownCaip19Id } from '../../../constants/solana';
3+
import { trackError } from '../../../utils/errors';
4+
import {
5+
getInterfaceContext,
6+
getPreferences,
7+
updateInterface,
8+
} from '../../../utils/interface';
9+
import { refreshSend } from './refreshSend';
10+
11+
jest.mock('../../../utils/errors', () => ({
12+
trackError: jest.fn().mockResolvedValue('tracked-error-id'),
13+
}));
14+
15+
jest.mock('../../../utils/interface', () => ({
16+
getInterfaceContext: jest.fn(),
17+
getPreferences: jest.fn(),
18+
SEND_FORM_INTERFACE_NAME: 'send-form',
19+
updateInterface: jest.fn(),
20+
}));
21+
22+
jest.mock('../../../../features/send/render', () => ({
23+
DEFAULT_SEND_CONTEXT: {
24+
preferences: {
25+
locale: 'en',
26+
currency: 'usd',
27+
},
28+
},
29+
}));
30+
31+
jest.mock('../../../../features/send/Send', () => ({
32+
Send: () => null,
33+
}));
34+
35+
jest.mock('../../../../snapContext', () => ({
36+
assetsService: {
37+
getAll: jest.fn(),
38+
},
39+
priceApiClient: {
40+
getMultipleSpotPrices: jest.fn(),
41+
},
42+
state: {
43+
getKey: jest.fn(),
44+
setKey: jest.fn(),
45+
},
46+
}));
47+
48+
const setupTest = () => {
49+
(globalThis as any).snap = {
50+
request: jest.fn(),
51+
};
52+
53+
(assetsService.getAll as jest.Mock).mockResolvedValue([
54+
{
55+
assetType: KnownCaip19Id.SolMainnet,
56+
},
57+
] as any);
58+
(state.getKey as jest.Mock).mockResolvedValue({
59+
'send-form': 'interface-id',
60+
});
61+
(getPreferences as jest.Mock).mockResolvedValue({
62+
locale: 'en',
63+
currency: 'usd',
64+
});
65+
(getInterfaceContext as jest.Mock).mockResolvedValue({
66+
tokenPrices: {},
67+
});
68+
(updateInterface as jest.Mock).mockResolvedValue(undefined);
69+
};
70+
71+
describe('refreshSend', () => {
72+
it('tracks background refresh errors', async () => {
73+
setupTest();
74+
75+
const error = new Error('Price API failed');
76+
77+
(priceApiClient.getMultipleSpotPrices as jest.Mock).mockRejectedValue(
78+
error,
79+
);
80+
81+
await refreshSend({ request: {} as any });
82+
83+
expect(trackError).toHaveBeenCalledWith(error);
84+
expect(updateInterface).not.toHaveBeenCalled();
85+
});
86+
});

packages/snap/src/core/handlers/onCronjob/backgroundEvents/refreshSend.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Send } from '../../../../features/send/Send';
55
import type { SendContext } from '../../../../features/send/types';
66
import { assetsService, priceApiClient, state } from '../../../../snapContext';
77
import type { UnencryptedStateValue } from '../../../services/state/State';
8+
import { trackError } from '../../../utils/errors';
89
import {
910
getInterfaceContext,
1011
getPreferences,
@@ -89,6 +90,7 @@ export const refreshSend: OnCronjobHandler = async () => {
8990
params: { duration: 'PT30S', request: { method: 'refreshSend' } },
9091
});
9192
} catch (error) {
93+
await trackError(error);
9294
logger.warn({ error }, `Could not refresh send interface`);
9395
}
9496
};

packages/snap/src/core/handlers/onKeyringRequest/Keyring.test.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
MOCK_SOLANA_SEED_PHRASE_2_KEYRING_ACCOUNT_1,
4747
} from '../../test/mocks/solana-keyring-accounts';
4848
import { getBip32EntropyMock } from '../../test/mocks/utils/getBip32Entropy';
49+
import { trackError } from '../../utils/errors';
4950
import { getBip32Entropy } from '../../utils/getBip32Entropy';
5051
import logger from '../../utils/logger';
5152
import { SolanaKeyring } from './Keyring';
@@ -59,6 +60,10 @@ jest.mock('../../utils/getBip32Entropy', () => ({
5960
getBip32Entropy: getBip32EntropyMock,
6061
}));
6162

63+
jest.mock('../../utils/errors', () => ({
64+
trackError: jest.fn().mockResolvedValue('tracked-error-id'),
65+
}));
66+
6267
const NON_EXISTENT_ACCOUNT_ID = '123e4567-e89b-12d3-a456-426614174009';
6368

6469
(globalThis as any).snap = {
@@ -738,20 +743,23 @@ describe('SolanaKeyring', () => {
738743
it('returns null when an error occurs', async () => {
739744
const mockScope = Network.Localnet;
740745
const mockRequest = {
741-
method: 'someMethod',
742-
params: [],
746+
id: 1,
747+
jsonrpc: '2.0',
748+
...MOCK_SIGN_AND_SEND_TRANSACTION_REQUEST,
743749
} as unknown as JsonRpcRequest;
750+
const error = new Error('Something went wrong');
744751

745752
jest
746753
.spyOn(mockWalletService, 'resolveAccountAddress')
747-
.mockRejectedValue(new Error('Something went wrong'));
754+
.mockRejectedValue(error);
748755

749756
const result = await keyring.resolveAccountAddress(
750757
mockScope,
751758
mockRequest,
752759
);
753760

754761
expect(result).toBeNull();
762+
expect(trackError).toHaveBeenCalledWith(error);
755763
});
756764
});
757765

packages/snap/src/core/handlers/onKeyringRequest/Keyring.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import {
6060
deriveSolanaKeypair,
6161
deriveSolanaKeypairFromCoinTypeNode,
6262
} from '../../utils/deriveSolanaKeypair';
63+
import { trackError } from '../../utils/errors';
6364
import { getBip32Entropy } from '../../utils/getBip32Entropy';
6465
import { getLowestUnusedIndex } from '../../utils/getLowestUnusedIndex';
6566
import {
@@ -802,6 +803,7 @@ export class SolanaKeyring implements Keyring {
802803

803804
return { address: caip10Address };
804805
} catch (error: any) {
806+
await trackError(error);
805807
this.#logger.error({ error }, 'Error resolving account address');
806808
return null;
807809
}

packages/snap/src/core/services/analytics/AnalyticsService.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@ import type { Transaction } from '@metamask/keyring-api';
44

55
import { Network } from '../../constants/solana';
66
import { MOCK_SOLANA_KEYRING_ACCOUNT_0 } from '../../test/mocks/solana-keyring-accounts';
7+
import { trackError } from '../../utils/errors';
78
import { ScanStatus, SecurityAlertResponse } from '../transaction-scan/types';
89
import { AnalyticsService } from './AnalyticsService';
910

11+
jest.mock('../../utils/errors', () => ({
12+
trackError: jest.fn().mockResolvedValue('tracked-error-id'),
13+
}));
14+
1015
const mockSnapRequest = jest.fn();
1116
const snap = {
1217
request: mockSnapRequest,
@@ -53,6 +58,18 @@ describe('AnalyticsService', () => {
5358
},
5459
});
5560
});
61+
62+
it('tracks analytics failures', async () => {
63+
const error = new Error('Tracking failed');
64+
mockSnapRequest.mockRejectedValueOnce(error);
65+
66+
await analyticsService.trackEventTransactionAdded(
67+
mockAccount,
68+
mockMetadata,
69+
);
70+
71+
expect(trackError).toHaveBeenCalledWith(error);
72+
});
5673
});
5774

5875
describe('trackEventTransactionApproved', () => {

0 commit comments

Comments
 (0)