mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 09:57:02 +01:00
Capture Sentry errors prior to initialization (#20265)
* Capture Sentry errors prior to initialization Sentry errors captured before/during the wallet initialization are currently not captured because we don't have the controller state yet to determine whether the user has consented. The Sentry setup has been updated to check the persisted state for whether the user has consented, as a fallback in case the controller state hasn't been initialized yet. This ensures that we capture errors during initialization if the user has opted in. * Always await async check for whether the user has opted in * Remove unused import * Update JSDoc return type * Remove unused driver method * Fix metametrics controller unit tests * Fix e2e tests * Fix e2e test on Firefox * Start session upon install rather than toggle
This commit is contained in:
parent
523b3145b2
commit
507c2cb475
@ -2,6 +2,10 @@
|
|||||||
* @file The entry point for the web extension singleton process.
|
* @file The entry point for the web extension singleton process.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// This import sets up a global function required for Sentry to function.
|
||||||
|
// It must be run first in case an error is thrown later during initialization.
|
||||||
|
import './lib/setup-persisted-state-hook';
|
||||||
|
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import endOfStream from 'end-of-stream';
|
import endOfStream from 'end-of-stream';
|
||||||
import pump from 'pump';
|
import pump from 'pump';
|
||||||
|
@ -419,18 +419,18 @@ export default class MetaMetricsController {
|
|||||||
*
|
*
|
||||||
* @param {boolean} participateInMetaMetrics - Whether or not the user wants
|
* @param {boolean} participateInMetaMetrics - Whether or not the user wants
|
||||||
* to participate in MetaMetrics
|
* to participate in MetaMetrics
|
||||||
* @returns {string|null} the string of the new metametrics id, or null
|
* @returns {Promise<string|null>} the string of the new metametrics id, or null
|
||||||
* if not set
|
* if not set
|
||||||
*/
|
*/
|
||||||
setParticipateInMetaMetrics(participateInMetaMetrics) {
|
async setParticipateInMetaMetrics(participateInMetaMetrics) {
|
||||||
let { metaMetricsId } = this.state;
|
let { metaMetricsId } = this.state;
|
||||||
if (participateInMetaMetrics && !metaMetricsId) {
|
if (participateInMetaMetrics && !metaMetricsId) {
|
||||||
// We also need to start sentry automatic session tracking at this point
|
// We also need to start sentry automatic session tracking at this point
|
||||||
globalThis.sentry?.startSession();
|
await globalThis.sentry?.startSession();
|
||||||
metaMetricsId = this.generateMetaMetricsId();
|
metaMetricsId = this.generateMetaMetricsId();
|
||||||
} else if (participateInMetaMetrics === false) {
|
} else if (participateInMetaMetrics === false) {
|
||||||
// We also need to stop sentry automatic session tracking at this point
|
// We also need to stop sentry automatic session tracking at this point
|
||||||
globalThis.sentry?.endSession();
|
await globalThis.sentry?.endSession();
|
||||||
metaMetricsId = null;
|
metaMetricsId = null;
|
||||||
}
|
}
|
||||||
this.store.updateState({ participateInMetaMetrics, metaMetricsId });
|
this.store.updateState({ participateInMetaMetrics, metaMetricsId });
|
||||||
|
@ -313,30 +313,30 @@ describe('MetaMetricsController', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('setParticipateInMetaMetrics', function () {
|
describe('setParticipateInMetaMetrics', function () {
|
||||||
it('should update the value of participateInMetaMetrics', function () {
|
it('should update the value of participateInMetaMetrics', async function () {
|
||||||
const metaMetricsController = getMetaMetricsController({
|
const metaMetricsController = getMetaMetricsController({
|
||||||
participateInMetaMetrics: null,
|
participateInMetaMetrics: null,
|
||||||
metaMetricsId: null,
|
metaMetricsId: null,
|
||||||
});
|
});
|
||||||
assert.equal(metaMetricsController.state.participateInMetaMetrics, null);
|
assert.equal(metaMetricsController.state.participateInMetaMetrics, null);
|
||||||
metaMetricsController.setParticipateInMetaMetrics(true);
|
await metaMetricsController.setParticipateInMetaMetrics(true);
|
||||||
assert.ok(globalThis.sentry.startSession.calledOnce);
|
assert.ok(globalThis.sentry.startSession.calledOnce);
|
||||||
assert.equal(metaMetricsController.state.participateInMetaMetrics, true);
|
assert.equal(metaMetricsController.state.participateInMetaMetrics, true);
|
||||||
metaMetricsController.setParticipateInMetaMetrics(false);
|
await metaMetricsController.setParticipateInMetaMetrics(false);
|
||||||
assert.equal(metaMetricsController.state.participateInMetaMetrics, false);
|
assert.equal(metaMetricsController.state.participateInMetaMetrics, false);
|
||||||
});
|
});
|
||||||
it('should generate and update the metaMetricsId when set to true', function () {
|
it('should generate and update the metaMetricsId when set to true', async function () {
|
||||||
const metaMetricsController = getMetaMetricsController({
|
const metaMetricsController = getMetaMetricsController({
|
||||||
participateInMetaMetrics: null,
|
participateInMetaMetrics: null,
|
||||||
metaMetricsId: null,
|
metaMetricsId: null,
|
||||||
});
|
});
|
||||||
assert.equal(metaMetricsController.state.metaMetricsId, null);
|
assert.equal(metaMetricsController.state.metaMetricsId, null);
|
||||||
metaMetricsController.setParticipateInMetaMetrics(true);
|
await metaMetricsController.setParticipateInMetaMetrics(true);
|
||||||
assert.equal(typeof metaMetricsController.state.metaMetricsId, 'string');
|
assert.equal(typeof metaMetricsController.state.metaMetricsId, 'string');
|
||||||
});
|
});
|
||||||
it('should nullify the metaMetricsId when set to false', function () {
|
it('should nullify the metaMetricsId when set to false', async function () {
|
||||||
const metaMetricsController = getMetaMetricsController();
|
const metaMetricsController = getMetaMetricsController();
|
||||||
metaMetricsController.setParticipateInMetaMetrics(false);
|
await metaMetricsController.setParticipateInMetaMetrics(false);
|
||||||
assert.ok(globalThis.sentry.endSession.calledOnce);
|
assert.ok(globalThis.sentry.endSession.calledOnce);
|
||||||
assert.equal(metaMetricsController.state.metaMetricsId, null);
|
assert.equal(metaMetricsController.state.metaMetricsId, null);
|
||||||
});
|
});
|
||||||
|
@ -29,7 +29,7 @@ export class FilterEvents implements Integration {
|
|||||||
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics
|
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics
|
||||||
* is enabled, `false` otherwise.
|
* is enabled, `false` otherwise.
|
||||||
*/
|
*/
|
||||||
private getMetaMetricsEnabled: () => boolean;
|
private getMetaMetricsEnabled: () => Promise<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param options - Constructor options.
|
* @param options - Constructor options.
|
||||||
@ -40,7 +40,7 @@ export class FilterEvents implements Integration {
|
|||||||
constructor({
|
constructor({
|
||||||
getMetaMetricsEnabled,
|
getMetaMetricsEnabled,
|
||||||
}: {
|
}: {
|
||||||
getMetaMetricsEnabled: () => boolean;
|
getMetaMetricsEnabled: () => Promise<boolean>;
|
||||||
}) {
|
}) {
|
||||||
this.getMetaMetricsEnabled = getMetaMetricsEnabled;
|
this.getMetaMetricsEnabled = getMetaMetricsEnabled;
|
||||||
}
|
}
|
||||||
@ -56,13 +56,13 @@ export class FilterEvents implements Integration {
|
|||||||
addGlobalEventProcessor: (callback: EventProcessor) => void,
|
addGlobalEventProcessor: (callback: EventProcessor) => void,
|
||||||
getCurrentHub: () => Hub,
|
getCurrentHub: () => Hub,
|
||||||
): void {
|
): void {
|
||||||
addGlobalEventProcessor((currentEvent: SentryEvent) => {
|
addGlobalEventProcessor(async (currentEvent: SentryEvent) => {
|
||||||
// Sentry integrations use the Sentry hub to get "this" references, for
|
// Sentry integrations use the Sentry hub to get "this" references, for
|
||||||
// reasons I don't fully understand.
|
// reasons I don't fully understand.
|
||||||
// eslint-disable-next-line consistent-this
|
// eslint-disable-next-line consistent-this
|
||||||
const self = getCurrentHub().getIntegration(FilterEvents);
|
const self = getCurrentHub().getIntegration(FilterEvents);
|
||||||
if (self) {
|
if (self) {
|
||||||
if (!self.getMetaMetricsEnabled()) {
|
if (!(await self.getMetaMetricsEnabled())) {
|
||||||
logger.warn(`Event dropped due to MetaMetrics setting.`);
|
logger.warn(`Event dropped due to MetaMetrics setting.`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
10
app/scripts/lib/setup-persisted-state-hook.js
Normal file
10
app/scripts/lib/setup-persisted-state-hook.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import LocalStore from './local-store';
|
||||||
|
import ReadOnlyNetworkStore from './network-store';
|
||||||
|
|
||||||
|
const localStore = process.env.IN_TEST
|
||||||
|
? new ReadOnlyNetworkStore()
|
||||||
|
: new LocalStore();
|
||||||
|
|
||||||
|
globalThis.stateHooks.getPersistedState = async function () {
|
||||||
|
return await localStore.get();
|
||||||
|
};
|
@ -118,16 +118,20 @@ export default function setupSentry({ release, getState }) {
|
|||||||
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics
|
* @returns `true` if MetaMask's state has been initialized, and MetaMetrics
|
||||||
* is enabled, `false` otherwise.
|
* is enabled, `false` otherwise.
|
||||||
*/
|
*/
|
||||||
function getMetaMetricsEnabled() {
|
async function getMetaMetricsEnabled() {
|
||||||
if (getState) {
|
|
||||||
const appState = getState();
|
const appState = getState();
|
||||||
if (!appState?.store?.metamask?.participateInMetaMetrics) {
|
if (Object.keys(appState) > 0) {
|
||||||
|
return Boolean(appState?.store?.metamask?.participateInMetaMetrics);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const persistedState = await globalThis.stateHooks.getPersistedState();
|
||||||
|
return Boolean(
|
||||||
|
persistedState?.data?.MetaMetricsController?.participateInMetaMetrics,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
@ -186,10 +190,10 @@ export default function setupSentry({ release, getState }) {
|
|||||||
* opted into MetaMetrics, change the autoSessionTracking option and start
|
* opted into MetaMetrics, change the autoSessionTracking option and start
|
||||||
* a new sentry session.
|
* a new sentry session.
|
||||||
*/
|
*/
|
||||||
const startSession = () => {
|
const startSession = async () => {
|
||||||
const hub = Sentry.getCurrentHub?.();
|
const hub = Sentry.getCurrentHub?.();
|
||||||
const options = hub.getClient?.().getOptions?.() ?? {};
|
const options = hub.getClient?.().getOptions?.() ?? {};
|
||||||
if (hub && getMetaMetricsEnabled() === true) {
|
if (hub && (await getMetaMetricsEnabled()) === true) {
|
||||||
options.autoSessionTracking = true;
|
options.autoSessionTracking = true;
|
||||||
hub.startSession();
|
hub.startSession();
|
||||||
}
|
}
|
||||||
@ -200,10 +204,10 @@ export default function setupSentry({ release, getState }) {
|
|||||||
* opted out of MetaMetrics, change the autoSessionTracking option and end
|
* opted out of MetaMetrics, change the autoSessionTracking option and end
|
||||||
* the current sentry session.
|
* the current sentry session.
|
||||||
*/
|
*/
|
||||||
const endSession = () => {
|
const endSession = async () => {
|
||||||
const hub = Sentry.getCurrentHub?.();
|
const hub = Sentry.getCurrentHub?.();
|
||||||
const options = hub.getClient?.().getOptions?.() ?? {};
|
const options = hub.getClient?.().getOptions?.() ?? {};
|
||||||
if (hub && getMetaMetricsEnabled() === false) {
|
if (hub && (await getMetaMetricsEnabled()) === false) {
|
||||||
options.autoSessionTracking = false;
|
options.autoSessionTracking = false;
|
||||||
hub.endSession();
|
hub.endSession();
|
||||||
}
|
}
|
||||||
@ -214,22 +218,22 @@ export default function setupSentry({ release, getState }) {
|
|||||||
* on the state of metaMetrics optin and the state of autoSessionTracking on
|
* on the state of metaMetrics optin and the state of autoSessionTracking on
|
||||||
* the Sentry client.
|
* the Sentry client.
|
||||||
*/
|
*/
|
||||||
const toggleSession = () => {
|
const toggleSession = async () => {
|
||||||
const hub = Sentry.getCurrentHub?.();
|
const hub = Sentry.getCurrentHub?.();
|
||||||
const options = hub.getClient?.().getOptions?.() ?? {
|
const options = hub.getClient?.().getOptions?.() ?? {
|
||||||
autoSessionTracking: false,
|
autoSessionTracking: false,
|
||||||
};
|
};
|
||||||
const isMetaMetricsEnabled = getMetaMetricsEnabled();
|
const isMetaMetricsEnabled = await getMetaMetricsEnabled();
|
||||||
if (
|
if (
|
||||||
isMetaMetricsEnabled === true &&
|
isMetaMetricsEnabled === true &&
|
||||||
options.autoSessionTracking === false
|
options.autoSessionTracking === false
|
||||||
) {
|
) {
|
||||||
startSession();
|
await startSession();
|
||||||
} else if (
|
} else if (
|
||||||
isMetaMetricsEnabled === false &&
|
isMetaMetricsEnabled === false &&
|
||||||
options.autoSessionTracking === true
|
options.autoSessionTracking === true
|
||||||
) {
|
) {
|
||||||
endSession();
|
await endSession();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -10,17 +10,16 @@ global.sentry = setupSentry({
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* As soon as state is available via getSentryState we can call the
|
* As soon as state is available via getSentryState we can start automatic
|
||||||
* toggleSession method added to sentry in setupSentry to start automatic
|
|
||||||
* session tracking.
|
* session tracking.
|
||||||
*/
|
*/
|
||||||
function waitForStateHooks() {
|
async function waitForStateHooks() {
|
||||||
if (global.stateHooks.getSentryState) {
|
if (global.stateHooks.getSentryState) {
|
||||||
// sentry is not defined in dev mode, so if sentry doesn't exist at this
|
// sentry is not defined in dev mode, so if sentry doesn't exist at this
|
||||||
// point it means that we are in dev mode and do not need to toggle the
|
// point it means that we are in dev mode and do not need to toggle the
|
||||||
// session. Using optional chaining is sufficient to prevent the error in
|
// session. Using optional chaining is sufficient to prevent the error in
|
||||||
// development.
|
// development.
|
||||||
global.sentry?.toggleSession();
|
await global.sentry?.startSession();
|
||||||
} else {
|
} else {
|
||||||
setTimeout(waitForStateHooks, 100);
|
setTimeout(waitForStateHooks, 100);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
// dev only, "react-devtools" import is skipped in prod builds
|
// dev only, "react-devtools" import is skipped in prod builds
|
||||||
import 'react-devtools';
|
import 'react-devtools';
|
||||||
|
|
||||||
|
// This import sets up a global function required for Sentry to function.
|
||||||
|
// It must be run first in case an error is thrown later during initialization.
|
||||||
|
import './lib/setup-persisted-state-hook';
|
||||||
|
|
||||||
import PortStream from 'extension-port-stream';
|
import PortStream from 'extension-port-stream';
|
||||||
import browser from 'webextension-polyfill';
|
import browser from 'webextension-polyfill';
|
||||||
|
|
||||||
|
@ -1,9 +1,26 @@
|
|||||||
const { strict: assert } = require('assert');
|
const { strict: assert } = require('assert');
|
||||||
|
const { Browser } = require('selenium-webdriver');
|
||||||
const { convertToHexValue, withFixtures } = require('../helpers');
|
const { convertToHexValue, withFixtures } = require('../helpers');
|
||||||
const FixtureBuilder = require('../fixture-builder');
|
const FixtureBuilder = require('../fixture-builder');
|
||||||
|
|
||||||
describe('Sentry errors', function () {
|
describe('Sentry errors', function () {
|
||||||
async function mockSentry(mockServer) {
|
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
|
return await mockServer
|
||||||
.forPost('https://sentry.io/api/0000000/envelope/')
|
.forPost('https://sentry.io/api/0000000/envelope/')
|
||||||
.withBodyIncluding('Test Error')
|
.withBodyIncluding('Test Error')
|
||||||
@ -23,6 +40,81 @@ describe('Sentry errors', function () {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
describe('before initialization', function () {
|
||||||
|
it('should NOT send error events when participateInMetaMetrics is false', 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 send error events', 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');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('after initialization', function () {
|
||||||
it('should NOT send error events when participateInMetaMetrics is false', async function () {
|
it('should NOT send error events when participateInMetaMetrics is false', async function () {
|
||||||
await withFixtures(
|
await withFixtures(
|
||||||
{
|
{
|
||||||
@ -35,7 +127,7 @@ describe('Sentry errors', function () {
|
|||||||
ganacheOptions,
|
ganacheOptions,
|
||||||
title: this.test.title,
|
title: this.test.title,
|
||||||
failOnConsoleError: false,
|
failOnConsoleError: false,
|
||||||
testSpecificMock: mockSentry,
|
testSpecificMock: mockSentryTestError,
|
||||||
},
|
},
|
||||||
async ({ driver, mockedEndpoint }) => {
|
async ({ driver, mockedEndpoint }) => {
|
||||||
await driver.navigate();
|
await driver.navigate();
|
||||||
@ -65,7 +157,7 @@ describe('Sentry errors', function () {
|
|||||||
ganacheOptions,
|
ganacheOptions,
|
||||||
title: this.test.title,
|
title: this.test.title,
|
||||||
failOnConsoleError: false,
|
failOnConsoleError: false,
|
||||||
testSpecificMock: mockSentry,
|
testSpecificMock: mockSentryTestError,
|
||||||
},
|
},
|
||||||
async ({ driver, mockedEndpoint }) => {
|
async ({ driver, mockedEndpoint }) => {
|
||||||
await driver.navigate();
|
await driver.navigate();
|
||||||
@ -92,4 +184,5 @@ describe('Sentry errors', function () {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user