1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-23 02:10:12 +01:00
metamask-extension/test/e2e/tests/errors.spec.js
Mark Stacey 63a0ae765f
Use unflattened state for Sentry (#20428)
The unflattened background state is now attached to any Sentry errors
from the background process. This provides a clearer picture of the
state of the wallet, and unblocks further improvements to Sentry state
which will come in later PRs.
2023-08-16 15:21:18 -02:30

614 lines
20 KiB
JavaScript

const { resolve } = require('path');
const { promises: fs } = require('fs');
const { strict: assert } = require('assert');
const { get, has, set } = require('lodash');
const { Browser } = require('selenium-webdriver');
const { format } = require('prettier');
const { convertToHexValue, withFixtures } = require('../helpers');
const FixtureBuilder = require('../fixture-builder');
const backgroundDateFields = ['CurrencyController.conversionDate'];
const uiDateFields = ['metamask.conversionDate'];
/**
* Transform date properties to value types, to ensure that state is
* consistent between test runs.
*
* @param {unknown} data - The data to transform
*/
function transformBackgroundDates(data) {
for (const field of backgroundDateFields) {
if (has(data, field)) {
set(data, field, typeof get(data, field));
}
}
return data;
}
/**
* Transform date properties to value types, to ensure that state is
* consistent between test runs.
*
* @param {unknown} data - The data to transform
*/
function transformUiDates(data) {
for (const field of uiDateFields) {
if (has(data, field)) {
set(data, field, typeof get(data, field));
}
}
return data;
}
/**
* Check that the data provided matches the snapshot.
*
* @param {object }args - Function arguments.
* @param {any} args.data - The data to compare with the snapshot.
* @param {string} args.snapshot - The name of the snapshot.
* @param {boolean} [args.update] - Whether to update the snapshot if it doesn't match.
*/
async function matchesSnapshot({
data,
snapshot,
update = process.env.UPDATE_SNAPSHOTS === 'true',
}) {
const snapshotPath = resolve(__dirname, `./state-snapshots/${snapshot}.json`);
const rawSnapshotData = await fs.readFile(snapshotPath, {
encoding: 'utf-8',
});
const snapshotData = JSON.parse(rawSnapshotData);
try {
assert.deepStrictEqual(data, snapshotData);
} catch (error) {
if (update && error instanceof assert.AssertionError) {
const stringifiedData = JSON.stringify(data);
// filepath specified so that Prettier can infer which parser to use
// from the file extension
const formattedData = format(stringifiedData, {
filepath: 'something.json',
});
await fs.writeFile(snapshotPath, formattedData, {
encoding: 'utf-8',
});
console.log(`Snapshot '${snapshot}' updated`);
return;
}
throw error;
}
}
describe('Sentry errors', function () {
const migrationError =
process.env.SELENIUM_BROWSER === Browser.CHROME
? `Cannot read properties of undefined (reading 'version')`
: 'meta is undefined';
async function mockSentryMigratorError(mockServer) {
return await mockServer
.forPost('https://sentry.io/api/0000000/envelope/')
.withBodyIncluding(migrationError)
.thenCallback(() => {
return {
statusCode: 200,
json: {},
};
});
}
async function mockSentryTestError(mockServer) {
return await mockServer
.forPost('https://sentry.io/api/0000000/envelope/')
.withBodyIncluding('Test Error')
.thenCallback(() => {
return {
statusCode: 200,
json: {},
};
});
}
const ganacheOptions = {
accounts: [
{
secretKey:
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
balance: convertToHexValue(25000000000000000000),
},
],
};
describe('before initialization, after opting out of metrics', function () {
it('should NOT send error events in the background', async function () {
await withFixtures(
{
fixtures: {
...new FixtureBuilder()
.withMetaMetricsController({
metaMetricsId: null,
participateInMetaMetrics: false,
})
.build(),
// Intentionally corrupt state to trigger migration error during initialization
meta: undefined,
},
ganacheOptions,
title: this.test.title,
failOnConsoleError: false,
testSpecificMock: mockSentryMigratorError,
},
async ({ driver, mockedEndpoint }) => {
await driver.navigate();
// Wait for Sentry request
await driver.delay(3000);
const isPending = await mockedEndpoint.isPending();
assert.ok(
isPending,
'A request to sentry was sent when it should not have been',
);
},
);
});
it('should NOT send error events in the UI', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder()
.withMetaMetricsController({
metaMetricsId: null,
participateInMetaMetrics: false,
})
.build(),
ganacheOptions,
title: this.test.title,
failOnConsoleError: false,
testSpecificMock: mockSentryTestError,
},
async ({ driver, mockedEndpoint }) => {
await driver.navigate();
await driver.findElement('#password');
// Erase `getSentryState` hook, simulating a "before initialization" state
await driver.executeScript(
'window.stateHooks.getSentryState = undefined',
);
// Wait for Sentry request
await driver.delay(3000);
const isPending = await mockedEndpoint.isPending();
assert.ok(
isPending,
'A request to sentry was sent when it should not have been',
);
},
);
});
});
describe('before initialization, after opting into metrics', function () {
it('should send error events in background', async function () {
await withFixtures(
{
fixtures: {
...new FixtureBuilder()
.withMetaMetricsController({
metaMetricsId: 'fake-metrics-id',
participateInMetaMetrics: true,
})
.build(),
// Intentionally corrupt state to trigger migration error during initialization
meta: undefined,
},
ganacheOptions,
title: this.test.title,
failOnConsoleError: false,
testSpecificMock: mockSentryMigratorError,
},
async ({ driver, mockedEndpoint }) => {
await driver.navigate();
// Wait for Sentry request
await driver.wait(async () => {
const isPending = await mockedEndpoint.isPending();
return isPending === false;
}, 3000);
const [mockedRequest] = await mockedEndpoint.getSeenRequests();
const mockTextBody = mockedRequest.body.text.split('\n');
const mockJsonBody = JSON.parse(mockTextBody[2]);
const { level } = mockJsonBody;
const [{ type, value }] = mockJsonBody.exception.values;
// Verify request
assert.equal(type, 'TypeError');
assert(value.includes(migrationError));
assert.equal(level, 'error');
},
);
});
it('should capture background application state', async function () {
await withFixtures(
{
fixtures: {
...new FixtureBuilder()
.withMetaMetricsController({
metaMetricsId: 'fake-metrics-id',
participateInMetaMetrics: true,
})
.build(),
// Intentionally corrupt state to trigger migration error during initialization
meta: undefined,
},
ganacheOptions,
title: this.test.title,
failOnConsoleError: false,
testSpecificMock: mockSentryMigratorError,
},
async ({ driver, mockedEndpoint }) => {
await driver.navigate();
// Wait for Sentry request
await driver.wait(async () => {
const isPending = await mockedEndpoint.isPending();
return isPending === false;
}, 3000);
const [mockedRequest] = await mockedEndpoint.getSeenRequests();
const mockTextBody = mockedRequest.body.text.split('\n');
const mockJsonBody = JSON.parse(mockTextBody[2]);
const appState = mockJsonBody?.extra?.appState;
await matchesSnapshot({
data: transformBackgroundDates(appState),
snapshot: 'errors-before-init-opt-in-background-state',
});
},
);
});
it('should send error events in UI', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder()
.withMetaMetricsController({
metaMetricsId: 'fake-metrics-id',
participateInMetaMetrics: true,
})
.build(),
ganacheOptions,
title: this.test.title,
failOnConsoleError: false,
testSpecificMock: mockSentryTestError,
},
async ({ driver, mockedEndpoint }) => {
await driver.navigate();
await driver.findElement('#password');
// Erase `getSentryState` hook, simulating a "before initialization" state
await driver.executeScript(
'window.stateHooks.getSentryState = undefined',
);
// Trigger error
await driver.executeScript('window.stateHooks.throwTestError()');
// Wait for Sentry request
await driver.wait(async () => {
const isPending = await mockedEndpoint.isPending();
return isPending === false;
}, 3000);
const [mockedRequest] = await mockedEndpoint.getSeenRequests();
const mockTextBody = mockedRequest.body.text.split('\n');
const mockJsonBody = JSON.parse(mockTextBody[2]);
const { level } = mockJsonBody;
const [{ type, value }] = mockJsonBody.exception.values;
// Verify request
assert.equal(type, 'TestError');
assert.equal(value, 'Test Error');
assert.equal(level, 'error');
},
);
});
it('should capture UI application state', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder()
.withMetaMetricsController({
metaMetricsId: 'fake-metrics-id',
participateInMetaMetrics: true,
})
.build(),
ganacheOptions,
title: this.test.title,
failOnConsoleError: false,
testSpecificMock: mockSentryTestError,
},
async ({ driver, mockedEndpoint }) => {
await driver.navigate();
await driver.findElement('#password');
// Erase `getSentryState` hook, simulating a "before initialization" state
await driver.executeScript(
'window.stateHooks.getSentryState = undefined',
);
// Trigger error
await driver.executeScript('window.stateHooks.throwTestError()');
// Wait for Sentry request
await driver.wait(async () => {
const isPending = await mockedEndpoint.isPending();
return isPending === false;
}, 3000);
const [mockedRequest] = await mockedEndpoint.getSeenRequests();
const mockTextBody = mockedRequest.body.text.split('\n');
const mockJsonBody = JSON.parse(mockTextBody[2]);
const appState = mockJsonBody?.extra?.appState;
await matchesSnapshot({
data: transformUiDates(appState),
snapshot: 'errors-before-init-opt-in-ui-state',
});
},
);
});
});
describe('after initialization, after opting out of metrics', function () {
it('should NOT send error events in the background', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder()
.withMetaMetricsController({
metaMetricsId: null,
participateInMetaMetrics: false,
})
.build(),
ganacheOptions,
title: this.test.title,
failOnConsoleError: false,
testSpecificMock: mockSentryTestError,
},
async ({ driver, mockedEndpoint }) => {
await driver.navigate();
await driver.findElement('#password');
// Trigger error
await driver.executeScript(
'window.stateHooks.throwTestBackgroundError()',
);
// Wait for Sentry request
const isPending = await mockedEndpoint.isPending();
assert.ok(
isPending,
'A request to sentry was sent when it should not have been',
);
},
);
});
it('should NOT send error events in the UI', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder()
.withMetaMetricsController({
metaMetricsId: null,
participateInMetaMetrics: false,
})
.build(),
ganacheOptions,
title: this.test.title,
failOnConsoleError: false,
testSpecificMock: mockSentryTestError,
},
async ({ driver, mockedEndpoint }) => {
await driver.navigate();
await driver.findElement('#password');
// Trigger error
await driver.executeScript('window.stateHooks.throwTestError()');
// Wait for Sentry request
const isPending = await mockedEndpoint.isPending();
assert.ok(
isPending,
'A request to sentry was sent when it should not have been',
);
},
);
});
});
describe('after initialization, after opting into metrics', function () {
it('should send error events in background', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder()
.withMetaMetricsController({
metaMetricsId: 'fake-metrics-id',
participateInMetaMetrics: true,
})
.build(),
ganacheOptions,
title: this.test.title,
failOnConsoleError: false,
testSpecificMock: mockSentryTestError,
},
async ({ driver, mockedEndpoint }) => {
await driver.navigate();
await driver.findElement('#password');
// Trigger error
await driver.executeScript(
'window.stateHooks.throwTestBackgroundError()',
);
// Wait for Sentry request
await driver.wait(async () => {
const isPending = await mockedEndpoint.isPending();
return isPending === false;
}, 3000);
const [mockedRequest] = await mockedEndpoint.getSeenRequests();
const mockTextBody = mockedRequest.body.text.split('\n');
const mockJsonBody = JSON.parse(mockTextBody[2]);
const { level, extra } = mockJsonBody;
const [{ type, value }] = mockJsonBody.exception.values;
const { participateInMetaMetrics } =
extra.appState.store.MetaMetricsController;
// Verify request
assert.equal(type, 'TestError');
assert.equal(value, 'Test Error');
assert.equal(level, 'error');
assert.equal(participateInMetaMetrics, true);
},
);
});
it('should capture background application state', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder()
.withMetaMetricsController({
metaMetricsId: 'fake-metrics-id',
participateInMetaMetrics: true,
})
.build(),
ganacheOptions,
title: this.test.title,
failOnConsoleError: false,
testSpecificMock: mockSentryTestError,
},
async ({ driver, mockedEndpoint }) => {
await driver.navigate();
await driver.findElement('#password');
// Trigger error
await driver.executeScript(
'window.stateHooks.throwTestBackgroundError()',
);
// Wait for Sentry request
await driver.wait(async () => {
const isPending = await mockedEndpoint.isPending();
return isPending === false;
}, 3000);
const [mockedRequest] = await mockedEndpoint.getSeenRequests();
const mockTextBody = mockedRequest.body.text.split('\n');
const mockJsonBody = JSON.parse(mockTextBody[2]);
const appState = mockJsonBody?.extra?.appState;
assert.deepStrictEqual(Object.keys(appState), [
'browser',
'store',
'version',
]);
assert.ok(
typeof appState?.browser === 'string' &&
appState?.browser.length > 0,
'Invalid browser state',
);
assert.ok(
typeof appState?.version === 'string' &&
appState?.version.length > 0,
'Invalid version state',
);
await matchesSnapshot({
data: transformBackgroundDates(appState.store),
snapshot: 'errors-after-init-opt-in-background-state',
});
},
);
});
it('should send error events in UI', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder()
.withMetaMetricsController({
metaMetricsId: 'fake-metrics-id',
participateInMetaMetrics: true,
})
.build(),
ganacheOptions,
title: this.test.title,
failOnConsoleError: false,
testSpecificMock: mockSentryTestError,
},
async ({ driver, mockedEndpoint }) => {
await driver.navigate();
await driver.findElement('#password');
// Trigger error
await driver.executeScript('window.stateHooks.throwTestError()');
// Wait for Sentry request
await driver.wait(async () => {
const isPending = await mockedEndpoint.isPending();
return isPending === false;
}, 3000);
const [mockedRequest] = await mockedEndpoint.getSeenRequests();
const mockTextBody = mockedRequest.body.text.split('\n');
const mockJsonBody = JSON.parse(mockTextBody[2]);
const { level, extra } = mockJsonBody;
const [{ type, value }] = mockJsonBody.exception.values;
const { participateInMetaMetrics } = extra.appState.store.metamask;
// Verify request
assert.equal(type, 'TestError');
assert.equal(value, 'Test Error');
assert.equal(level, 'error');
assert.equal(participateInMetaMetrics, true);
},
);
});
it('should capture UI application state', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder()
.withMetaMetricsController({
metaMetricsId: 'fake-metrics-id',
participateInMetaMetrics: true,
})
.build(),
ganacheOptions,
title: this.test.title,
failOnConsoleError: false,
testSpecificMock: mockSentryTestError,
},
async ({ driver, mockedEndpoint }) => {
await driver.navigate();
await driver.findElement('#password');
// Trigger error
await driver.executeScript('window.stateHooks.throwTestError()');
// Wait for Sentry request
await driver.wait(async () => {
const isPending = await mockedEndpoint.isPending();
return isPending === false;
}, 3000);
const [mockedRequest] = await mockedEndpoint.getSeenRequests();
const mockTextBody = mockedRequest.body.text.split('\n');
const mockJsonBody = JSON.parse(mockTextBody[2]);
const appState = mockJsonBody?.extra?.appState;
assert.deepStrictEqual(Object.keys(appState), [
'browser',
'store',
'version',
]);
assert.ok(
typeof appState?.browser === 'string' &&
appState?.browser.length > 0,
'Invalid browser state',
);
assert.ok(
typeof appState?.version === 'string' &&
appState?.version.length > 0,
'Invalid version state',
);
await matchesSnapshot({
data: transformUiDates(appState.store),
snapshot: 'errors-after-init-opt-in-ui-state',
});
},
);
});
});
});