Skip to content

Commit 87528de

Browse files
authored
feat: Add Snap export handler usage tracking (#3281)
This PR moves MetaMetrics tracking of Snap export usage to Snap Controller. Related issue: #3259 Extension integration PR: MetaMask/metamask-extension#31553 Mobile integration PR: MetaMask/metamask-mobile#14428
1 parent 37e79af commit 87528de

File tree

10 files changed

+541
-116
lines changed

10 files changed

+541
-116
lines changed
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"branches": 93.51,
3-
"functions": 98.14,
4-
"lines": 98.48,
5-
"statements": 98.31
2+
"branches": 93.61,
3+
"functions": 98.16,
4+
"lines": 98.5,
5+
"statements": 98.33
66
}

packages/snaps-controllers/src/snaps/SnapController.test.tsx

Lines changed: 190 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -4702,7 +4702,7 @@ describe('SnapController', () => {
47024702
},
47034703
});
47044704

4705-
expect(rootMessenger.call).toHaveBeenCalledTimes(4);
4705+
expect(rootMessenger.call).toHaveBeenCalledTimes(5);
47064706
expect(rootMessenger.call).toHaveBeenCalledWith(
47074707
'ExecutionService:handleRpcRequest',
47084708
MOCK_SNAP_ID,
@@ -9214,46 +9214,6 @@ describe('SnapController', () => {
92149214
});
92159215
});
92169216

9217-
describe('getRegistryMetadata', () => {
9218-
it('returns the metadata for a verified snap', async () => {
9219-
const registry = new MockSnapsRegistry();
9220-
const rootMessenger = getControllerMessenger(registry);
9221-
const messenger = getSnapControllerMessenger(rootMessenger);
9222-
registry.getMetadata.mockReturnValue({
9223-
name: 'Mock Snap',
9224-
});
9225-
9226-
const snapController = getSnapController(
9227-
getSnapControllerOptions({
9228-
messenger,
9229-
}),
9230-
);
9231-
9232-
expect(
9233-
await snapController.getRegistryMetadata(MOCK_SNAP_ID),
9234-
).toStrictEqual({
9235-
name: 'Mock Snap',
9236-
});
9237-
9238-
snapController.destroy();
9239-
});
9240-
9241-
it('returns null for a non-verified snap', async () => {
9242-
const registry = new MockSnapsRegistry();
9243-
const rootMessenger = getControllerMessenger(registry);
9244-
const messenger = getSnapControllerMessenger(rootMessenger);
9245-
const snapController = getSnapController(
9246-
getSnapControllerOptions({
9247-
messenger,
9248-
}),
9249-
);
9250-
9251-
expect(await snapController.getRegistryMetadata(MOCK_SNAP_ID)).toBeNull();
9252-
9253-
snapController.destroy();
9254-
});
9255-
});
9256-
92579217
describe('clearState', () => {
92589218
it('clears the state and terminates running snaps', async () => {
92599219
const rootMessenger = getControllerMessenger();
@@ -9416,6 +9376,195 @@ describe('SnapController', () => {
94169376

94179377
snapController.destroy();
94189378
});
9379+
9380+
it('should track event for allowed handler', async () => {
9381+
const mockTrackEvent = jest.fn();
9382+
const rootMessenger = getControllerMessenger();
9383+
const executionEnvironmentStub = new ExecutionEnvironmentStub(
9384+
getNodeEESMessenger(rootMessenger),
9385+
) as unknown as NodeThreadExecutionService;
9386+
9387+
const [snapController] = getSnapControllerWithEES(
9388+
getSnapControllerWithEESOptions({
9389+
rootMessenger,
9390+
trackEvent: mockTrackEvent,
9391+
state: {
9392+
snaps: getPersistedSnapsState(),
9393+
},
9394+
}),
9395+
executionEnvironmentStub,
9396+
);
9397+
9398+
const snap = snapController.getExpect(MOCK_SNAP_ID);
9399+
await snapController.startSnap(snap.id);
9400+
9401+
await snapController.handleRequest({
9402+
snapId: snap.id,
9403+
origin: MOCK_ORIGIN,
9404+
handler: HandlerType.OnRpcRequest,
9405+
request: {
9406+
jsonrpc: '2.0',
9407+
method: 'test',
9408+
params: {},
9409+
id: 1,
9410+
},
9411+
});
9412+
9413+
expect(mockTrackEvent).toHaveBeenCalledTimes(1);
9414+
expect(mockTrackEvent).toHaveBeenCalledWith({
9415+
event: 'SnapExportUsed',
9416+
category: 'Snaps',
9417+
properties: {
9418+
export: 'onRpcRequest',
9419+
origin: 'https://example.com',
9420+
// eslint-disable-next-line @typescript-eslint/naming-convention
9421+
snap_category: null,
9422+
// eslint-disable-next-line @typescript-eslint/naming-convention
9423+
snap_id: 'npm:@metamask/example-snap',
9424+
success: true,
9425+
},
9426+
});
9427+
snapController.destroy();
9428+
});
9429+
9430+
it('should not track event for disallowed handler', async () => {
9431+
const mockTrackEvent = jest.fn();
9432+
const rootMessenger = getControllerMessenger();
9433+
9434+
rootMessenger.registerActionHandler(
9435+
'PermissionController:getPermissions',
9436+
() => ({
9437+
[SnapEndowments.Cronjob]: {
9438+
caveats: [
9439+
{ type: SnapCaveatType.SnapCronjob, value: '* * * * *' },
9440+
],
9441+
date: 1664187844588,
9442+
id: 'izn0WGUO8cvq_jqvLQuQP',
9443+
invoker: MOCK_SNAP_ID,
9444+
parentCapability: SnapEndowments.Cronjob,
9445+
},
9446+
}),
9447+
);
9448+
9449+
const executionEnvironmentStub = new ExecutionEnvironmentStub(
9450+
getNodeEESMessenger(rootMessenger),
9451+
) as unknown as NodeThreadExecutionService;
9452+
9453+
const [snapController] = getSnapControllerWithEES(
9454+
getSnapControllerWithEESOptions({
9455+
environmentEndowmentPermissions: ['endowment:cronjob'],
9456+
rootMessenger,
9457+
trackEvent: mockTrackEvent,
9458+
state: {
9459+
snaps: getPersistedSnapsState(),
9460+
},
9461+
}),
9462+
executionEnvironmentStub,
9463+
);
9464+
9465+
const snap = snapController.getExpect(MOCK_SNAP_ID);
9466+
await snapController.startSnap(snap.id);
9467+
9468+
await snapController.handleRequest({
9469+
snapId: snap.id,
9470+
origin: MOCK_ORIGIN,
9471+
handler: HandlerType.OnCronjob,
9472+
request: {
9473+
jsonrpc: '2.0',
9474+
method: 'test',
9475+
params: {},
9476+
id: 1,
9477+
},
9478+
});
9479+
9480+
expect(mockTrackEvent).not.toHaveBeenCalled();
9481+
snapController.destroy();
9482+
});
9483+
9484+
it('should properly handle error when MetaMetrics hook throws an error', async () => {
9485+
const log = jest.spyOn(console, 'error').mockImplementation();
9486+
const error = new Error('MetaMetrics hook error');
9487+
const mockTrackEvent = jest.fn().mockImplementation(() => {
9488+
throw error;
9489+
});
9490+
const rootMessenger = getControllerMessenger();
9491+
const executionEnvironmentStub = new ExecutionEnvironmentStub(
9492+
getNodeEESMessenger(rootMessenger),
9493+
) as unknown as NodeThreadExecutionService;
9494+
9495+
const [snapController] = getSnapControllerWithEES(
9496+
getSnapControllerWithEESOptions({
9497+
rootMessenger,
9498+
trackEvent: mockTrackEvent,
9499+
state: {
9500+
snaps: getPersistedSnapsState(),
9501+
},
9502+
}),
9503+
executionEnvironmentStub,
9504+
);
9505+
9506+
const snap = snapController.getExpect(MOCK_SNAP_ID);
9507+
await snapController.startSnap(snap.id);
9508+
9509+
await snapController.handleRequest({
9510+
snapId: snap.id,
9511+
origin: MOCK_ORIGIN,
9512+
handler: HandlerType.OnRpcRequest,
9513+
request: {
9514+
jsonrpc: '2.0',
9515+
method: 'test',
9516+
params: {},
9517+
id: 1,
9518+
},
9519+
});
9520+
9521+
expect(mockTrackEvent).toHaveBeenCalled();
9522+
expect(log).toHaveBeenCalledWith(
9523+
expect.stringContaining(
9524+
'Error when calling MetaMetrics hook for snap',
9525+
),
9526+
);
9527+
snapController.destroy();
9528+
});
9529+
9530+
it('should not track event for preinstalled snap', async () => {
9531+
const mockTrackEvent = jest.fn();
9532+
const rootMessenger = getControllerMessenger();
9533+
const executionEnvironmentStub = new ExecutionEnvironmentStub(
9534+
getNodeEESMessenger(rootMessenger),
9535+
) as unknown as NodeThreadExecutionService;
9536+
9537+
const [snapController] = getSnapControllerWithEES(
9538+
getSnapControllerWithEESOptions({
9539+
rootMessenger,
9540+
trackEvent: mockTrackEvent,
9541+
state: {
9542+
snaps: getPersistedSnapsState(
9543+
getPersistedSnapObject({ preinstalled: true }),
9544+
),
9545+
},
9546+
}),
9547+
executionEnvironmentStub,
9548+
);
9549+
9550+
const snap = snapController.getExpect(MOCK_SNAP_ID);
9551+
await snapController.startSnap(snap.id);
9552+
9553+
await snapController.handleRequest({
9554+
snapId: snap.id,
9555+
origin: MOCK_ORIGIN,
9556+
handler: HandlerType.OnRpcRequest,
9557+
request: {
9558+
jsonrpc: '2.0',
9559+
method: 'test',
9560+
params: {},
9561+
id: 1,
9562+
},
9563+
});
9564+
9565+
expect(mockTrackEvent).not.toHaveBeenCalled();
9566+
snapController.destroy();
9567+
});
94199568
});
94209569

94219570
it('handles a transaction insight request', async () => {
@@ -10388,35 +10537,6 @@ describe('SnapController', () => {
1038810537
});
1038910538
});
1039010539

10391-
describe('SnapController:getRegistryMetadata', () => {
10392-
it('calls SnapController.getRegistryMetadata()', async () => {
10393-
const registry = new MockSnapsRegistry();
10394-
const rootMessenger = getControllerMessenger(registry);
10395-
const messenger = getSnapControllerMessenger(rootMessenger);
10396-
10397-
registry.getMetadata.mockReturnValue({
10398-
name: 'Mock Snap',
10399-
});
10400-
10401-
const snapController = getSnapController(
10402-
getSnapControllerOptions({
10403-
messenger,
10404-
}),
10405-
);
10406-
10407-
expect(
10408-
await messenger.call(
10409-
'SnapController:getRegistryMetadata',
10410-
MOCK_SNAP_ID,
10411-
),
10412-
).toStrictEqual({
10413-
name: 'Mock Snap',
10414-
});
10415-
10416-
snapController.destroy();
10417-
});
10418-
});
10419-
1042010540
describe('SnapController:disconnectOrigin', () => {
1042110541
it('calls SnapController.removeSnapFromSubject()', () => {
1042210542
const messenger = getSnapControllerMessenger();

0 commit comments

Comments
 (0)