1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

Set sentry autoTrackSessions default (#20132)

* Set sentry autoTrackSessions default

* endSession....

* fixup

* updated comment

* prevent breaking devmode

* remove changes to beforeSend

* remove additional usage of sinon
This commit is contained in:
Brad Decker 2023-07-26 07:13:28 -05:00 committed by GitHub
parent 59e0f7b506
commit e28db07a1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 211 additions and 3 deletions

View File

@ -425,8 +425,12 @@ export default class MetaMetricsController {
setParticipateInMetaMetrics(participateInMetaMetrics) { 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
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
globalThis.sentry?.endSession();
metaMetricsId = null; metaMetricsId = null;
} }
this.store.updateState({ participateInMetaMetrics, metaMetricsId }); this.store.updateState({ participateInMetaMetrics, metaMetricsId });

View File

@ -146,6 +146,14 @@ describe('MetaMetricsController', function () {
const now = new Date(); const now = new Date();
let clock; let clock;
beforeEach(function () { beforeEach(function () {
globalThis.sentry = {
startSession: sinon.fake(() => {
/** NOOP */
}),
endSession: sinon.fake(() => {
/** NOOP */
}),
};
clock = sinon.useFakeTimers(now.getTime()); clock = sinon.useFakeTimers(now.getTime());
sinon.stub(Utils, 'generateRandomId').returns('DUMMY_RANDOM_ID'); sinon.stub(Utils, 'generateRandomId').returns('DUMMY_RANDOM_ID');
}); });
@ -312,6 +320,7 @@ describe('MetaMetricsController', function () {
}); });
assert.equal(metaMetricsController.state.participateInMetaMetrics, null); assert.equal(metaMetricsController.state.participateInMetaMetrics, null);
metaMetricsController.setParticipateInMetaMetrics(true); metaMetricsController.setParticipateInMetaMetrics(true);
assert.ok(globalThis.sentry.startSession.calledOnce);
assert.equal(metaMetricsController.state.participateInMetaMetrics, true); assert.equal(metaMetricsController.state.participateInMetaMetrics, true);
metaMetricsController.setParticipateInMetaMetrics(false); metaMetricsController.setParticipateInMetaMetrics(false);
assert.equal(metaMetricsController.state.participateInMetaMetrics, false); assert.equal(metaMetricsController.state.participateInMetaMetrics, false);
@ -328,6 +337,7 @@ describe('MetaMetricsController', function () {
it('should nullify the metaMetricsId when set to false', function () { it('should nullify the metaMetricsId when set to false', function () {
const metaMetricsController = getMetaMetricsController(); const metaMetricsController = getMetaMetricsController();
metaMetricsController.setParticipateInMetaMetrics(false); metaMetricsController.setParticipateInMetaMetrics(false);
assert.ok(globalThis.sentry.endSession.calledOnce);
assert.equal(metaMetricsController.state.metaMetricsId, null); assert.equal(metaMetricsController.state.metaMetricsId, null);
}); });
}); });

View File

@ -133,8 +133,45 @@ export default function setupSentry({ release, getState }) {
Sentry.init({ Sentry.init({
dsn: sentryTarget, dsn: sentryTarget,
debug: METAMASK_DEBUG, debug: METAMASK_DEBUG,
/**
* autoSessionTracking defaults to true and operates by sending a session
* packet to sentry. This session packet does not appear to be filtered out
* via our beforeSend or FilterEvents integration. To avoid sending a
* request before we have the state tree and can validate the users
* preferences, we initiate this to false. Later, in startSession and
* endSession we modify this option and start the session or end the
* session manually.
*
* In sentry-install we call toggleSession after the page loads and state
* is available, this handles initiating the session for a user who has
* opted into MetaMetrics. This script is ran in both the background and UI
* so it should be effective at starting the session in both places.
*
* In the MetaMetricsController the session is manually started or stopped
* when the user opts in or out of MetaMetrics. This occurs in the
* setParticipateInMetaMetrics function which is exposed to the UI via the
* MetaMaskController.
*
* In actions.ts, after sending the updated participateInMetaMetrics flag
* to the background, we call toggleSession to ensure sentry is kept in
* sync with the user's preference.
*
* Types for the global Sentry object, and the new methods added as part of
* this effort were added to global.d.ts in the types folder.
*/
autoSessionTracking: false,
environment, environment,
integrations: [ integrations: [
/**
* Filtering of events must happen in this FilterEvents custom
* integration instead of in the beforeSend handler because the Dedupe
* integration is unaware of the beforeSend functionality. If an event is
* queued in the sentry context, additional events of the same name will
* be filtered out by Dedupe even if the original event was not sent due
* to the beforeSend method returning null.
*
* @see https://github.com/MetaMask/metamask-extension/pull/15677
*/
new FilterEvents({ getMetaMetricsEnabled }), new FilterEvents({ getMetaMetricsEnabled }),
new Dedupe(), new Dedupe(),
new ExtraErrorData(), new ExtraErrorData(),
@ -144,7 +181,64 @@ export default function setupSentry({ release, getState }) {
beforeBreadcrumb: beforeBreadcrumb(getState), beforeBreadcrumb: beforeBreadcrumb(getState),
}); });
return Sentry; /**
* As long as a reference to the Sentry Hub can be found, and the user has
* opted into MetaMetrics, change the autoSessionTracking option and start
* a new sentry session.
*/
const startSession = () => {
const hub = Sentry.getCurrentHub?.();
const options = hub.getClient?.().getOptions?.() ?? {};
if (hub && getMetaMetricsEnabled() === true) {
options.autoSessionTracking = true;
hub.startSession();
}
};
/**
* As long as a reference to the Sentry Hub can be found, and the user has
* opted out of MetaMetrics, change the autoSessionTracking option and end
* the current sentry session.
*/
const endSession = () => {
const hub = Sentry.getCurrentHub?.();
const options = hub.getClient?.().getOptions?.() ?? {};
if (hub && getMetaMetricsEnabled() === false) {
options.autoSessionTracking = false;
hub.endSession();
}
};
/**
* Call the appropriate method (either startSession or endSession) depending
* on the state of metaMetrics optin and the state of autoSessionTracking on
* the Sentry client.
*/
const toggleSession = () => {
const hub = Sentry.getCurrentHub?.();
const options = hub.getClient?.().getOptions?.() ?? {
autoSessionTracking: false,
};
const isMetaMetricsEnabled = getMetaMetricsEnabled();
if (
isMetaMetricsEnabled === true &&
options.autoSessionTracking === false
) {
startSession();
} else if (
isMetaMetricsEnabled === false &&
options.autoSessionTracking === true
) {
endSession();
}
};
return {
...Sentry,
startSession,
endSession,
toggleSession,
};
} }
/** /**

View File

@ -8,3 +8,22 @@ global.sentry = setupSentry({
release: process.env.METAMASK_VERSION, release: process.env.METAMASK_VERSION,
getState: () => global.stateHooks?.getSentryState?.() || {}, getState: () => global.stateHooks?.getSentryState?.() || {},
}); });
/**
* As soon as state is available via getSentryState we can call the
* toggleSession method added to sentry in setupSentry to start automatic
* session tracking.
*/
function waitForStateHooks() {
if (global.stateHooks.getSentryState) {
// 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
// session. Using optional chaining is sufficient to prevent the error in
// development.
global.sentry?.toggleSession();
} else {
setTimeout(waitForStateHooks, 100);
}
}
waitForStateHooks();

View File

@ -23,6 +23,36 @@ describe('Sentry errors', function () {
}, },
], ],
}; };
it('should NOT send error events when participateInMetaMetrics is false', async function () {
await withFixtures(
{
fixtures: new FixtureBuilder()
.withMetaMetricsController({
metaMetricsId: null,
participateInMetaMetrics: false,
})
.build(),
ganacheOptions,
title: this.test.title,
failOnConsoleError: false,
testSpecificMock: mockSentry,
},
async ({ driver, mockedEndpoint }) => {
await driver.navigate();
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
// Trigger error
driver.executeScript('window.stateHooks.throwTestError()');
driver.delay(3000);
// 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 send error events', async function () { it('should send error events', async function () {
await withFixtures( await withFixtures(
{ {

16
types/global.d.ts vendored
View File

@ -1,6 +1,7 @@
// In order for variables to be considered on the global scope they must be // In order for variables to be considered on the global scope they must be
// declared using var and not const or let, which is why this rule is disabled // declared using var and not const or let, which is why this rule is disabled
/* eslint-disable no-var */ /* eslint-disable no-var */
import * as Sentry from '@sentry/browser';
declare class Platform { declare class Platform {
openTab: (opts: { url: string }) => void; openTab: (opts: { url: string }) => void;
@ -8,8 +9,23 @@ declare class Platform {
closeCurrentWindow: () => void; closeCurrentWindow: () => void;
} }
declare class SentryObject extends Sentry {
// Verifies that the user has opted into metrics and then updates the sentry
// instance to track sessions and begins the session.
startSession: () => void;
// Verifies that the user has opted out of metrics and then updates the
// sentry instance to NOT track sessions and ends the current session.
endSession: () => void;
// Calls either startSession or endSession based on optin status
toggleSession: () => void;
}
export declare global { export declare global {
var platform: Platform; var platform: Platform;
// Sentry is undefined in dev, so use optional chaining
var sentry: SentryObject | undefined;
namespace jest { namespace jest {
interface Matchers<R> { interface Matchers<R> {

View File

@ -195,9 +195,11 @@ function setupDebuggingHelpers(store) {
/** /**
* The following stateHook is a method intended to throw an error, used in * The following stateHook is a method intended to throw an error, used in
* our E2E test to ensure that errors are attempted to be sent to sentry. * our E2E test to ensure that errors are attempted to be sent to sentry.
*
* @param {string} [msg] - The error message to throw, defaults to 'Test Error'
*/ */
window.stateHooks.throwTestError = async function () { window.stateHooks.throwTestError = async function (msg = 'Test Error') {
const error = new Error('Test Error'); const error = new Error(msg);
error.name = 'TestError'; error.name = 'TestError';
throw error; throw error;
}; };

View File

@ -1619,6 +1619,32 @@ describe('Actions', () => {
}); });
}); });
describe('#setParticipateInMetaMetrics', () => {
beforeAll(() => {
window.sentry = {
toggleSession: jest.fn(),
endSession: jest.fn(),
};
});
it('sets participateInMetaMetrics to true', async () => {
const store = mockStore();
const setParticipateInMetaMetricsStub = jest.fn((_, cb) => cb());
background.getApi.returns({
setParticipateInMetaMetrics: setParticipateInMetaMetricsStub,
});
_setBackgroundConnection(background.getApi());
await store.dispatch(actions.setParticipateInMetaMetrics(true));
expect(setParticipateInMetaMetricsStub).toHaveBeenCalledWith(
true,
expect.anything(),
);
expect(window.sentry.toggleSession).toHaveBeenCalled();
});
});
describe('#setUseBlockie', () => { describe('#setUseBlockie', () => {
afterEach(() => { afterEach(() => {
sinon.restore(); sinon.restore();

View File

@ -1570,6 +1570,7 @@ export function updateMetamaskState(
}); });
} }
}); });
// Also emit an event for the selected account changing, either due to a // Also emit an event for the selected account changing, either due to a
// property update or if the entire account changes. // property update or if the entire account changes.
if (isEqual(oldSelectedAccount, newSelectedAccount) === false) { if (isEqual(oldSelectedAccount, newSelectedAccount) === false) {
@ -2883,6 +2884,12 @@ export function setParticipateInMetaMetrics(
reject(err); reject(err);
return; return;
} }
/**
* We need to inform sentry that the user's optin preference may have
* changed. The logic to determine which way to toggle is in the
* toggleSession handler in setupSentry.js.
*/
window.sentry?.toggleSession();
dispatch({ dispatch({
type: actionConstants.SET_PARTICIPATE_IN_METAMETRICS, type: actionConstants.SET_PARTICIPATE_IN_METAMETRICS,