Skip to content

Commit 5c4bf45

Browse files
authored
chore: adds errorTrackingTransport (#456)
In order to track failed RPC requests, we've added an `errorTrackingTransport` that wraps the existing transport pipeline to track failed RPC requests. It integrates with the current architecture and uses the new `snap_trackError` method to capture error details. This transport wrapper acts as a lightweight solution, for us to have better logs and debugging capabilities. Reference: MetaMask/snaps#3498 What it does: - Catches HTTP errors (4xx, 5xx) - Detects JSON-RPC errors that come back in 2xx responses - Preserves the original error flow so nothing breaks
1 parent b630fbd commit 5c4bf45

File tree

4 files changed

+457
-5
lines changed

4 files changed

+457
-5
lines changed
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import { createErrorTrackingTransport } from './errorTrackingTransport';
2+
3+
const mockSnap = {
4+
request: jest.fn(),
5+
};
6+
7+
describe('createErrorTrackingTransport', () => {
8+
beforeEach(() => {
9+
(globalThis as any).snap = mockSnap;
10+
jest.clearAllMocks();
11+
});
12+
13+
describe('HTTP errors (4xx, 5xx)', () => {
14+
it('should track HTTP 500 errors', async () => {
15+
const mockTransport = jest
16+
.fn()
17+
.mockRejectedValue(new Error('HTTP 500 Internal Server Error'));
18+
19+
const errorTrackingTransport =
20+
createErrorTrackingTransport(mockTransport);
21+
22+
await expect(
23+
errorTrackingTransport({ payload: { method: 'getBalance' } }),
24+
).rejects.toThrow('HTTP 500 Internal Server Error');
25+
26+
expect(mockSnap.request).toHaveBeenCalledWith({
27+
method: 'snap_trackError',
28+
params: {
29+
error: expect.objectContaining({
30+
message: expect.stringContaining('HTTP 500 Internal Server Error'),
31+
}),
32+
},
33+
});
34+
});
35+
36+
it('should track network timeout errors', async () => {
37+
const mockTransport = jest
38+
.fn()
39+
.mockRejectedValue(new Error('Network timeout'));
40+
41+
const errorTrackingTransport =
42+
createErrorTrackingTransport(mockTransport);
43+
44+
await expect(
45+
errorTrackingTransport({ payload: { method: 'getBalance' } }),
46+
).rejects.toThrow('Network timeout');
47+
48+
expect(mockSnap.request).toHaveBeenCalledWith({
49+
method: 'snap_trackError',
50+
params: {
51+
error: expect.objectContaining({
52+
message: expect.stringContaining('Network timeout'),
53+
}),
54+
},
55+
});
56+
});
57+
});
58+
59+
describe('JSON-RPC errors in 2xx responses', () => {
60+
it('should track standard JSON-RPC errors but return the response', async () => {
61+
const mockResponse = {
62+
jsonrpc: '2.0',
63+
id: 1,
64+
error: {
65+
code: -32000,
66+
message: 'RPC error: Invalid request',
67+
},
68+
};
69+
70+
const mockTransport = jest.fn().mockResolvedValue(mockResponse);
71+
72+
const errorTrackingTransport =
73+
createErrorTrackingTransport(mockTransport);
74+
75+
const result = await errorTrackingTransport({
76+
payload: { method: 'getBalance' },
77+
});
78+
79+
// Should return the response instead of throwing
80+
expect(result).toStrictEqual(mockResponse);
81+
82+
expect(mockSnap.request).toHaveBeenCalledWith({
83+
method: 'snap_trackError',
84+
params: {
85+
error: expect.objectContaining({
86+
message: expect.stringContaining('RPC error in response'),
87+
}),
88+
},
89+
});
90+
});
91+
});
92+
93+
describe('Successful responses', () => {
94+
it('should not track successful responses', async () => {
95+
const mockResponse = {
96+
jsonrpc: '2.0',
97+
id: 1,
98+
result: {
99+
value: 1000000,
100+
},
101+
};
102+
103+
const mockTransport = jest.fn().mockResolvedValue(mockResponse);
104+
105+
const errorTrackingTransport =
106+
createErrorTrackingTransport(mockTransport);
107+
108+
const result = await errorTrackingTransport({
109+
payload: { method: 'getBalance' },
110+
});
111+
112+
expect(result).toStrictEqual(mockResponse);
113+
expect(mockSnap.request).not.toHaveBeenCalled();
114+
});
115+
116+
it('should not track responses with null result but no error', async () => {
117+
const mockResponse = {
118+
jsonrpc: '2.0',
119+
id: 1,
120+
result: null,
121+
};
122+
123+
const mockTransport = jest.fn().mockResolvedValue(mockResponse);
124+
125+
const errorTrackingTransport =
126+
createErrorTrackingTransport(mockTransport);
127+
128+
const result = await errorTrackingTransport({
129+
payload: { method: 'getBalance' },
130+
});
131+
132+
expect(result).toStrictEqual(mockResponse);
133+
expect(mockSnap.request).not.toHaveBeenCalled();
134+
});
135+
});
136+
137+
describe('Error tracking failures', () => {
138+
it('should handle error tracking failures gracefully', async () => {
139+
const mockTransport = jest
140+
.fn()
141+
.mockRejectedValue(new Error('Network error'));
142+
mockSnap.request.mockRejectedValue(new Error('Tracking failed'));
143+
144+
const errorTrackingTransport =
145+
createErrorTrackingTransport(mockTransport);
146+
147+
await expect(
148+
errorTrackingTransport({ payload: { method: 'getBalance' } }),
149+
).rejects.toThrow('Network error');
150+
151+
expect(mockSnap.request).toHaveBeenCalled();
152+
});
153+
});
154+
155+
describe('Error information extraction', () => {
156+
it('should include method and URL in error tracking', async () => {
157+
const mockTransport = jest
158+
.fn()
159+
.mockRejectedValue(new Error('Test error'));
160+
161+
const errorTrackingTransport =
162+
createErrorTrackingTransport(mockTransport);
163+
164+
await expect(
165+
errorTrackingTransport({ payload: { method: 'getBalance' } }),
166+
).rejects.toThrow('Test error');
167+
168+
expect(mockSnap.request).toHaveBeenCalledWith({
169+
method: 'snap_trackError',
170+
params: {
171+
error: expect.objectContaining({
172+
message: expect.stringContaining('"method":"getBalance"'),
173+
}),
174+
},
175+
});
176+
});
177+
178+
it('should extract currentUrl from error object when available', async () => {
179+
const mockError = new Error('Network error');
180+
(mockError as any).currentUrl = 'https://api2.example.com';
181+
182+
const mockTransport = jest.fn().mockRejectedValue(mockError);
183+
184+
const errorTrackingTransport =
185+
createErrorTrackingTransport(mockTransport);
186+
187+
await expect(
188+
errorTrackingTransport({ payload: { method: 'getBalance' } }),
189+
).rejects.toThrow('Network error');
190+
191+
expect(mockSnap.request).toHaveBeenCalledWith({
192+
method: 'snap_trackError',
193+
params: {
194+
error: expect.objectContaining({
195+
message: expect.stringContaining(
196+
'"url":"https://api2.example.com"',
197+
),
198+
}),
199+
},
200+
});
201+
});
202+
203+
it('should extract status codes from errors', async () => {
204+
const mockError = new Error('HTTP 404 Not Found');
205+
(mockError as any).status = 404;
206+
207+
const mockTransport = jest.fn().mockRejectedValue(mockError);
208+
209+
const errorTrackingTransport =
210+
createErrorTrackingTransport(mockTransport);
211+
212+
await expect(
213+
errorTrackingTransport({ payload: { method: 'getBalance' } }),
214+
).rejects.toThrow('HTTP 404 Not Found');
215+
216+
expect(mockSnap.request).toHaveBeenCalledWith({
217+
method: 'snap_trackError',
218+
params: {
219+
error: expect.objectContaining({
220+
message: expect.stringContaining('"statusCode":404'),
221+
}),
222+
},
223+
});
224+
});
225+
});
226+
227+
describe('Different error formats', () => {
228+
it('should handle string errors', async () => {
229+
const mockTransport = jest.fn().mockRejectedValue('Simple string error');
230+
231+
const errorTrackingTransport =
232+
createErrorTrackingTransport(mockTransport);
233+
234+
await expect(
235+
errorTrackingTransport({ payload: { method: 'getBalance' } }),
236+
).rejects.toThrow('Simple string error');
237+
238+
expect(mockSnap.request).toHaveBeenCalledWith({
239+
method: 'snap_trackError',
240+
params: {
241+
error: expect.objectContaining({
242+
message: expect.stringContaining(
243+
'"errorMessage":"Simple string error"',
244+
),
245+
}),
246+
},
247+
});
248+
});
249+
250+
it('should handle objects with error property', async () => {
251+
const mockError = { error: { code: -32000, message: 'Server error' } };
252+
const mockTransport = jest.fn().mockRejectedValue(mockError);
253+
254+
const errorTrackingTransport =
255+
createErrorTrackingTransport(mockTransport);
256+
257+
await expect(
258+
errorTrackingTransport({ payload: { method: 'getBalance' } }),
259+
).rejects.toThrow('{"code":-32000,"message":"Server error"}');
260+
261+
expect(mockSnap.request).toHaveBeenCalledWith({
262+
method: 'snap_trackError',
263+
params: {
264+
error: expect.objectContaining({
265+
message: expect.stringContaining(
266+
'"errorMessage":"{\\"code\\":-32000,\\"message\\":\\"Server error\\"}"',
267+
),
268+
}),
269+
},
270+
});
271+
});
272+
});
273+
});

0 commit comments

Comments
 (0)