Skip to content

Commit dd7291c

Browse files
committed
Add snap_trackEvent method
1 parent 8cf11b2 commit dd7291c

File tree

6 files changed

+531
-0
lines changed

6 files changed

+531
-0
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { requestSnapsHandler } from './requestSnaps';
1818
import { resolveInterfaceHandler } from './resolveInterface';
1919
import { scheduleBackgroundEventHandler } from './scheduleBackgroundEvent';
2020
import { setStateHandler } from './setState';
21+
import { trackEventHandler } from './trackEvent';
2122
import { updateInterfaceHandler } from './updateInterface';
2223

2324
/* eslint-disable @typescript-eslint/naming-convention */
@@ -43,6 +44,7 @@ export const methodHandlers = {
4344
snap_cancelBackgroundEvent: cancelBackgroundEventHandler,
4445
snap_getBackgroundEvents: getBackgroundEventsHandler,
4546
snap_setState: setStateHandler,
47+
snap_trackEvent: trackEventHandler,
4648
};
4749
/* eslint-enable @typescript-eslint/naming-convention */
4850

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
import { JsonRpcEngine } from '@metamask/json-rpc-engine';
2+
import type { TrackEventParams, TrackEventResult } from '@metamask/snaps-sdk';
3+
import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils';
4+
5+
import { trackEventHandler } from './trackEvent';
6+
7+
/* eslint-disable @typescript-eslint/naming-convention */
8+
describe('snap_trackEvent', () => {
9+
describe('trackEventHandler', () => {
10+
it('has the expected shape', () => {
11+
expect(trackEventHandler).toMatchObject({
12+
methodNames: ['snap_trackEvent'],
13+
implementation: expect.any(Function),
14+
hookNames: {
15+
trackEvent: true,
16+
getSnap: true,
17+
},
18+
});
19+
});
20+
});
21+
22+
describe('implementation', () => {
23+
it('tracks an event with no properties', async () => {
24+
const { implementation } = trackEventHandler;
25+
26+
const trackEvent = jest.fn();
27+
const getSnap = jest.fn().mockReturnValue({ preinstalled: true });
28+
const hooks = { trackEvent, getSnap };
29+
30+
const engine = new JsonRpcEngine();
31+
32+
engine.push((request, response, next, end) => {
33+
const result = implementation(
34+
request as JsonRpcRequest<TrackEventParams>,
35+
response as PendingJsonRpcResponse<TrackEventResult>,
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_trackEvent',
48+
params: {
49+
event: {
50+
event: 'test_event',
51+
},
52+
},
53+
});
54+
55+
expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, result: null });
56+
expect(trackEvent).toHaveBeenCalledWith({
57+
event: 'test_event',
58+
});
59+
});
60+
61+
it('tracks an event with properties', async () => {
62+
const { implementation } = trackEventHandler;
63+
64+
const trackEvent = jest.fn();
65+
const getSnap = jest.fn().mockReturnValue({ preinstalled: true });
66+
const hooks = { trackEvent, getSnap };
67+
68+
const engine = new JsonRpcEngine();
69+
70+
engine.push((request, response, next, end) => {
71+
const result = implementation(
72+
request as JsonRpcRequest<TrackEventParams>,
73+
response as PendingJsonRpcResponse<TrackEventResult>,
74+
next,
75+
end,
76+
hooks,
77+
);
78+
79+
result?.catch(end);
80+
});
81+
82+
const response = await engine.handle({
83+
jsonrpc: '2.0',
84+
id: 1,
85+
method: 'snap_trackEvent',
86+
params: {
87+
event: {
88+
event: 'test_event',
89+
properties: {
90+
user_action: 'click',
91+
button_name: 'submit',
92+
},
93+
},
94+
},
95+
});
96+
97+
expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, result: null });
98+
expect(trackEvent).toHaveBeenCalledWith({
99+
event: 'test_event',
100+
properties: {
101+
user_action: 'click',
102+
button_name: 'submit',
103+
},
104+
});
105+
});
106+
107+
it('tracks an event with sensitive properties', async () => {
108+
const { implementation } = trackEventHandler;
109+
110+
const trackEvent = jest.fn();
111+
const getSnap = jest.fn().mockReturnValue({ preinstalled: true });
112+
const hooks = { trackEvent, getSnap };
113+
114+
const engine = new JsonRpcEngine();
115+
116+
engine.push((request, response, next, end) => {
117+
const result = implementation(
118+
request as JsonRpcRequest<TrackEventParams>,
119+
response as PendingJsonRpcResponse<TrackEventResult>,
120+
next,
121+
end,
122+
hooks,
123+
);
124+
125+
result?.catch(end);
126+
});
127+
128+
const response = await engine.handle({
129+
jsonrpc: '2.0',
130+
id: 1,
131+
method: 'snap_trackEvent',
132+
params: {
133+
event: {
134+
event: 'test_event',
135+
sensitiveProperties: {
136+
wallet_address: '0x123',
137+
transaction_hash: '0xabc',
138+
},
139+
},
140+
},
141+
});
142+
143+
expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, result: null });
144+
expect(trackEvent).toHaveBeenCalledWith({
145+
event: 'test_event',
146+
sensitiveProperties: {
147+
wallet_address: '0x123',
148+
transaction_hash: '0xabc',
149+
},
150+
});
151+
});
152+
153+
it('throws on properties with non-snake_case keys', async () => {
154+
const { implementation } = trackEventHandler;
155+
156+
const trackEvent = jest.fn();
157+
const getSnap = jest.fn().mockReturnValue({ preinstalled: true });
158+
const hooks = { trackEvent, getSnap };
159+
160+
const engine = new JsonRpcEngine();
161+
162+
engine.push((request, response, next, end) => {
163+
const result = implementation(
164+
request as JsonRpcRequest<TrackEventParams>,
165+
response as PendingJsonRpcResponse<TrackEventResult>,
166+
next,
167+
end,
168+
hooks,
169+
);
170+
171+
result?.catch(end);
172+
});
173+
174+
const response = await engine.handle({
175+
jsonrpc: '2.0',
176+
id: 1,
177+
method: 'snap_trackEvent',
178+
params: {
179+
event: {
180+
event: 'test_event',
181+
properties: {
182+
userAction: 'click',
183+
ButtonName: 'submit',
184+
},
185+
},
186+
},
187+
});
188+
189+
expect(response).toStrictEqual({
190+
error: {
191+
code: -32602,
192+
message:
193+
'Invalid params: All custom value keys must be in snake_case format in event.properties.',
194+
stack: expect.any(String),
195+
},
196+
id: 1,
197+
jsonrpc: '2.0',
198+
});
199+
expect(trackEvent).not.toHaveBeenCalled();
200+
});
201+
202+
it('throws on sensitive properties with non-snake_case keys', async () => {
203+
const { implementation } = trackEventHandler;
204+
205+
const trackEvent = jest.fn();
206+
const getSnap = jest.fn().mockReturnValue({ preinstalled: true });
207+
const hooks = { trackEvent, getSnap };
208+
209+
const engine = new JsonRpcEngine();
210+
211+
engine.push((request, response, next, end) => {
212+
const result = implementation(
213+
request as JsonRpcRequest<TrackEventParams>,
214+
response as PendingJsonRpcResponse<TrackEventResult>,
215+
next,
216+
end,
217+
hooks,
218+
);
219+
220+
result?.catch(end);
221+
});
222+
223+
const response = await engine.handle({
224+
jsonrpc: '2.0',
225+
id: 1,
226+
method: 'snap_trackEvent',
227+
params: {
228+
event: {
229+
event: 'test_event',
230+
sensitiveProperties: {
231+
walletAddress: '0x123',
232+
transactionHash: '0xabc',
233+
},
234+
},
235+
},
236+
});
237+
238+
expect(response).toStrictEqual({
239+
error: {
240+
code: -32602,
241+
message:
242+
'Invalid params: All custom value keys must be in snake_case format in event.sensitiveProperties.',
243+
stack: expect.any(String),
244+
},
245+
id: 1,
246+
jsonrpc: '2.0',
247+
});
248+
expect(trackEvent).not.toHaveBeenCalled();
249+
});
250+
251+
it('throws on missing event name', async () => {
252+
const { implementation } = trackEventHandler;
253+
254+
const trackEvent = jest.fn();
255+
const getSnap = jest.fn().mockReturnValue({ preinstalled: true });
256+
const hooks = { trackEvent, getSnap };
257+
258+
const engine = new JsonRpcEngine();
259+
260+
engine.push((request, response, next, end) => {
261+
const result = implementation(
262+
request as JsonRpcRequest<TrackEventParams>,
263+
response as PendingJsonRpcResponse<TrackEventResult>,
264+
next,
265+
end,
266+
hooks,
267+
);
268+
269+
result?.catch(end);
270+
});
271+
272+
const response = await engine.handle({
273+
jsonrpc: '2.0',
274+
id: 1,
275+
method: 'snap_trackEvent',
276+
params: {
277+
event: {},
278+
},
279+
});
280+
281+
expect(response).toStrictEqual({
282+
error: {
283+
code: -32602,
284+
message:
285+
'Invalid params: At path: event.event -- Expected a string, but received: undefined.',
286+
stack: expect.any(String),
287+
},
288+
id: 1,
289+
jsonrpc: '2.0',
290+
});
291+
expect(trackEvent).not.toHaveBeenCalled();
292+
});
293+
294+
it('throws if non-preinstalled snap is calling the method', async () => {
295+
const { implementation } = trackEventHandler;
296+
297+
const trackEvent = jest.fn();
298+
const getSnap = jest.fn().mockReturnValue({ preinstalled: false });
299+
const hooks = { trackEvent, getSnap };
300+
301+
const engine = new JsonRpcEngine();
302+
303+
engine.push((request, response, next, end) => {
304+
const result = implementation(
305+
request as JsonRpcRequest<TrackEventParams>,
306+
response as PendingJsonRpcResponse<TrackEventResult>,
307+
next,
308+
end,
309+
hooks,
310+
);
311+
312+
result?.catch(end);
313+
});
314+
315+
const response = await engine.handle({
316+
jsonrpc: '2.0',
317+
id: 1,
318+
method: 'snap_trackEvent',
319+
params: {
320+
event: {
321+
event: 'test_event',
322+
properties: {
323+
user_action: 'click',
324+
button_name: 'submit',
325+
},
326+
},
327+
},
328+
});
329+
330+
expect(response).toStrictEqual({
331+
error: {
332+
code: 4100,
333+
message:
334+
'The requested account and/or method has not been authorized by the user.',
335+
stack: expect.any(String),
336+
},
337+
id: 1,
338+
jsonrpc: '2.0',
339+
});
340+
expect(trackEvent).not.toHaveBeenCalled();
341+
});
342+
});
343+
});
344+
/* eslint-enable @typescript-eslint/naming-convention */

0 commit comments

Comments
 (0)