Skip to content

Commit a47bb43

Browse files
authored
feat: Add snap_startTrace and snap_endTrace methods for performance tracing (#3519)
This adds two methods, `snap_startTrace` and `snap_endTrace`, intended to hook into the performance tracing functionality of Sentry. They are essentially wrappers of the `trace` and `endTrace` functions in the clients.
1 parent 9ef5f0f commit a47bb43

File tree

15 files changed

+804
-7
lines changed

15 files changed

+804
-7
lines changed

packages/examples/packages/preinstalled/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/snaps.git"
88
},
99
"source": {
10-
"shasum": "orIGELPnJuNGSXC/IH7XrDciP6y+csrN9oWh//+m7DI=",
10+
"shasum": "yNJdBPGXVZJZHIjQtrlPGXA+BlDBZhjZIswZbTjuYkM=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",

packages/examples/packages/preinstalled/src/index.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,23 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
6464
});
6565
}
6666

67+
case 'startTrace':
68+
return await snap.request({
69+
method: 'snap_startTrace',
70+
params: {
71+
name: 'Test Snap Trace',
72+
},
73+
});
74+
75+
case 'endTrace': {
76+
return await snap.request({
77+
method: 'snap_endTrace',
78+
params: {
79+
name: 'Test Snap Trace',
80+
},
81+
});
82+
}
83+
6784
default:
6885
throw new MethodNotFoundError({ method: request.method });
6986
}

packages/snaps-rpc-methods/jest.config.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ module.exports = deepmerge(baseConfig, {
1010
],
1111
coverageThreshold: {
1212
global: {
13-
branches: 95.32,
14-
functions: 98.73,
15-
lines: 98.89,
16-
statements: 98.59,
13+
branches: 95.37,
14+
functions: 98.76,
15+
lines: 98.92,
16+
statements: 98.62,
1717
},
1818
},
1919
});
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { JsonRpcEngine } from '@metamask/json-rpc-engine';
2+
import type { EndTraceParams, EndTraceResult } from '@metamask/snaps-sdk';
3+
import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils';
4+
5+
import { endTraceHandler } from './endTrace';
6+
7+
describe('snap_endTrace', () => {
8+
describe('endTraceHandler', () => {
9+
it('has the expected shape', () => {
10+
expect(endTraceHandler).toMatchObject({
11+
methodNames: ['snap_endTrace'],
12+
implementation: expect.any(Function),
13+
hookNames: {
14+
endTrace: true,
15+
getSnap: true,
16+
},
17+
});
18+
});
19+
});
20+
21+
describe('implementation', () => {
22+
it('calls the `endTrace` hook with the provided parameters', async () => {
23+
const { implementation } = endTraceHandler;
24+
25+
const endTrace = jest.fn().mockReturnValue(null);
26+
27+
const getSnap = jest.fn().mockReturnValue({ preinstalled: true });
28+
const hooks = { endTrace, getSnap };
29+
30+
const engine = new JsonRpcEngine();
31+
32+
engine.push((request, response, next, end) => {
33+
const result = implementation(
34+
request as JsonRpcRequest<EndTraceParams>,
35+
response as PendingJsonRpcResponse<EndTraceResult>,
36+
next,
37+
end,
38+
hooks,
39+
);
40+
41+
result?.catch(end);
42+
});
43+
44+
const response = await engine.handle({
45+
jsonrpc: '2.0',
46+
id: 1,
47+
method: 'snap_endTrace',
48+
params: {
49+
id: 'test-id',
50+
name: 'Test Trace',
51+
timestamp: 1234567890,
52+
},
53+
});
54+
55+
expect(response).toStrictEqual({
56+
jsonrpc: '2.0',
57+
id: 1,
58+
result: null,
59+
});
60+
61+
expect(endTrace).toHaveBeenCalledWith({
62+
id: 'test-id',
63+
name: 'Test Trace',
64+
timestamp: 1234567890,
65+
});
66+
});
67+
68+
it('throws an error if the Snap is not preinstalled', async () => {
69+
const { implementation } = endTraceHandler;
70+
71+
const endTrace = jest.fn();
72+
const getSnap = jest.fn().mockReturnValue({ preinstalled: false });
73+
const hooks = { endTrace, getSnap };
74+
75+
const engine = new JsonRpcEngine();
76+
77+
engine.push((request, response, next, end) => {
78+
const result = implementation(
79+
request as JsonRpcRequest<EndTraceParams>,
80+
response as PendingJsonRpcResponse<EndTraceResult>,
81+
next,
82+
end,
83+
hooks,
84+
);
85+
86+
result?.catch(end);
87+
});
88+
89+
const response = await engine.handle({
90+
jsonrpc: '2.0',
91+
id: 1,
92+
method: 'snap_endTrace',
93+
params: {
94+
id: 'test-id',
95+
name: 'Test Trace',
96+
},
97+
});
98+
99+
expect(endTrace).not.toHaveBeenCalled();
100+
expect(response).toStrictEqual({
101+
jsonrpc: '2.0',
102+
id: 1,
103+
error: {
104+
code: -32601,
105+
message: 'The method does not exist / is not available.',
106+
stack: expect.any(String),
107+
},
108+
});
109+
});
110+
111+
it.each([
112+
[
113+
{ foo: 'bar' },
114+
'Invalid params: At path: name -- Expected a string, but received: undefined.',
115+
],
116+
[
117+
{ name: undefined },
118+
'Invalid params: At path: name -- Expected a string, but received: undefined.',
119+
],
120+
[
121+
{ name: 'Test Trace', id: 123 },
122+
'Invalid params: At path: id -- Expected a string, but received: 123.',
123+
],
124+
[
125+
{ name: 'Test Trace', id: 'test-id', timestamp: 'not-a-number' },
126+
'Invalid params: At path: timestamp -- Expected a number, but received: "not-a-number".',
127+
],
128+
])(
129+
'throws an error if the parameters are invalid',
130+
async (params, error) => {
131+
const { implementation } = endTraceHandler;
132+
133+
const endTrace = jest.fn();
134+
const getSnap = jest.fn().mockReturnValue({ preinstalled: true });
135+
const hooks = { endTrace, getSnap };
136+
137+
const engine = new JsonRpcEngine();
138+
139+
engine.push((request, response, next, end) => {
140+
const result = implementation(
141+
request as JsonRpcRequest<EndTraceParams>,
142+
response as PendingJsonRpcResponse<EndTraceResult>,
143+
next,
144+
end,
145+
hooks,
146+
);
147+
148+
result?.catch(end);
149+
});
150+
151+
// @ts-expect-error: Intentionally passing invalid params.
152+
// eslint-disable-next-line @typescript-eslint/await-thenable
153+
const response = await engine.handle({
154+
jsonrpc: '2.0',
155+
id: 1,
156+
method: 'snap_endTrace',
157+
params,
158+
});
159+
160+
expect(endTrace).not.toHaveBeenCalled();
161+
expect(response).toStrictEqual({
162+
jsonrpc: '2.0',
163+
id: 1,
164+
error: {
165+
code: -32602,
166+
message: error,
167+
stack: expect.any(String),
168+
},
169+
});
170+
},
171+
);
172+
});
173+
});
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine';
2+
import type { PermittedHandlerExport } from '@metamask/permission-controller';
3+
import { rpcErrors } from '@metamask/rpc-errors';
4+
import type {
5+
JsonRpcRequest,
6+
EndTraceParams,
7+
EndTraceResult,
8+
EndTraceRequest,
9+
} from '@metamask/snaps-sdk';
10+
import type { InferMatching, Snap } from '@metamask/snaps-utils';
11+
import {
12+
number,
13+
create,
14+
object,
15+
string,
16+
StructError,
17+
exactOptional,
18+
} from '@metamask/superstruct';
19+
import type { PendingJsonRpcResponse } from '@metamask/utils';
20+
21+
import type { MethodHooksObject } from '../utils';
22+
23+
const hookNames: MethodHooksObject<EndTraceMethodHooks> = {
24+
endTrace: true,
25+
getSnap: true,
26+
};
27+
28+
export type EndTraceMethodHooks = {
29+
/**
30+
* End a performance trace in Sentry.
31+
*
32+
* @param request - The trace request object.
33+
* @returns The performance trace context.
34+
*/
35+
endTrace: (request: EndTraceRequest) => void;
36+
37+
/**
38+
* Get Snap metadata.
39+
*
40+
* @param snapId - The ID of a Snap.
41+
*/
42+
getSnap: (snapId: string) => Snap | undefined;
43+
};
44+
45+
const EndTraceParametersStruct = object({
46+
id: exactOptional(string()),
47+
name: string(),
48+
timestamp: exactOptional(number()),
49+
});
50+
51+
export type EndTraceParameters = InferMatching<
52+
typeof EndTraceParametersStruct,
53+
EndTraceParams
54+
>;
55+
56+
/**
57+
* Handler for the `snap_endTrace` method.
58+
*/
59+
export const endTraceHandler: PermittedHandlerExport<
60+
EndTraceMethodHooks,
61+
EndTraceParameters,
62+
EndTraceResult
63+
> = {
64+
methodNames: ['snap_endTrace'],
65+
implementation: getEndTraceImplementation,
66+
hookNames,
67+
};
68+
69+
/**
70+
* The `snap_endTrace` method implementation. This method is used to end a
71+
* performance trace in Sentry. It is only available to preinstalled Snaps.
72+
*
73+
* @param request - The JSON-RPC request object.
74+
* @param response - The JSON-RPC response object.
75+
* @param _next - The `json-rpc-engine` "next" callback. Not used by this
76+
* function.
77+
* @param end - The `json-rpc-engine` "end" callback.
78+
* @param hooks - The RPC method hooks.
79+
* @param hooks.endTrace - The hook function to end a performance trace.
80+
* @param hooks.getSnap - The hook function to get Snap metadata.
81+
* @returns Nothing.
82+
*/
83+
function getEndTraceImplementation(
84+
request: JsonRpcRequest<EndTraceParameters>,
85+
response: PendingJsonRpcResponse,
86+
_next: unknown,
87+
end: JsonRpcEngineEndCallback,
88+
{ endTrace, getSnap }: EndTraceMethodHooks,
89+
): void {
90+
const snap = getSnap(
91+
(request as JsonRpcRequest<EndTraceParams> & { origin: string }).origin,
92+
);
93+
94+
if (!snap?.preinstalled) {
95+
return end(rpcErrors.methodNotFound());
96+
}
97+
98+
const { params } = request;
99+
100+
try {
101+
const validatedParams = getValidatedParams(params);
102+
endTrace(validatedParams);
103+
104+
response.result = null;
105+
} catch (error) {
106+
return end(error);
107+
}
108+
109+
return end();
110+
}
111+
112+
/**
113+
* Validate the parameters for the `snap_endTrace` method.
114+
*
115+
* @param params - Parameters to validate.
116+
* @returns Validated parameters.
117+
* @throws Throws RPC error if validation fails.
118+
*/
119+
function getValidatedParams(params: unknown): EndTraceParameters {
120+
try {
121+
return create(params, EndTraceParametersStruct);
122+
} catch (error) {
123+
if (error instanceof StructError) {
124+
throw rpcErrors.invalidParams({
125+
message: `Invalid params: ${error.message}.`,
126+
});
127+
}
128+
129+
/* istanbul ignore next */
130+
throw rpcErrors.internal();
131+
}
132+
}

packages/snaps-rpc-methods/src/permitted/handlers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { cancelBackgroundEventHandler } from './cancelBackgroundEvent';
22
import { clearStateHandler } from './clearState';
33
import { closeWebSocketHandler } from './closeWebSocket';
44
import { createInterfaceHandler } from './createInterface';
5+
import { endTraceHandler } from './endTrace';
56
import { providerRequestHandler } from './experimentalProviderRequest';
67
import { getAllSnapsHandler } from './getAllSnaps';
78
import { getBackgroundEventsHandler } from './getBackgroundEvents';
@@ -22,6 +23,7 @@ import { resolveInterfaceHandler } from './resolveInterface';
2223
import { scheduleBackgroundEventHandler } from './scheduleBackgroundEvent';
2324
import { sendWebSocketMessageHandler } from './sendWebSocketMessage';
2425
import { setStateHandler } from './setState';
26+
import { startTraceHandler } from './startTrace';
2527
import { trackErrorHandler } from './trackError';
2628
import { trackEventHandler } from './trackEvent';
2729
import { updateInterfaceHandler } from './updateInterface';
@@ -55,6 +57,8 @@ export const methodHandlers = {
5557
snap_closeWebSocket: closeWebSocketHandler,
5658
snap_sendWebSocketMessage: sendWebSocketMessageHandler,
5759
snap_getWebSockets: getWebSocketsHandler,
60+
snap_startTrace: startTraceHandler,
61+
snap_endTrace: endTraceHandler,
5862
};
5963
/* eslint-enable @typescript-eslint/naming-convention */
6064

0 commit comments

Comments
 (0)