Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add performance tracing infrastructure #26044

Merged
merged 46 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
cb608f6
Upgrade to Sentry 8.19.0
matthewwalsh0 Jul 22, 2024
4b949a8
Fix unit test
matthewwalsh0 Jul 22, 2024
b9c552f
Update LavaMoat policies
metamaskbot Jul 22, 2024
1e6a839
Fix Sentry sessions
matthewwalsh0 Jul 22, 2024
638c38c
Fix E2E tests
matthewwalsh0 Jul 22, 2024
325ebd3
Add comment
matthewwalsh0 Jul 22, 2024
1f0221d
Merge branch 'develop' into chore/sentry-upgrade-8
matthewwalsh0 Jul 22, 2024
a768719
Merge branch 'chore/sentry-upgrade-8' into fix/sentry-session-toggle
matthewwalsh0 Jul 22, 2024
341fe90
Fix E2E tests
matthewwalsh0 Jul 22, 2024
d1b5858
Merge branch 'chore/sentry-upgrade-8' into fix/sentry-session-toggle
matthewwalsh0 Jul 22, 2024
1c3563d
Add trace method
matthewwalsh0 Jul 22, 2024
a79f404
Add automated tracing
matthewwalsh0 Jul 22, 2024
468fac1
Fix unit tests
matthewwalsh0 Jul 22, 2024
d36b375
Remove default properties
matthewwalsh0 Jul 22, 2024
7213f51
Add test button icons
matthewwalsh0 Jul 23, 2024
dc9feb5
Simplify logic
matthewwalsh0 Jul 23, 2024
07f63af
Merge branch 'develop' into chore/sentry-upgrade-8
matthewwalsh0 Jul 23, 2024
6ca072d
Merge branch 'chore/sentry-upgrade-8' into fix/sentry-session-toggle
matthewwalsh0 Jul 23, 2024
1249353
Prevent Sentry trace if metrics disabled
matthewwalsh0 Jul 23, 2024
651f2f2
Fix E2E tests
matthewwalsh0 Jul 23, 2024
cc3f162
Merge branch 'develop' into chore/sentry-upgrade-8
matthewwalsh0 Jul 23, 2024
13dd393
Merge branch 'chore/sentry-upgrade-8' into fix/sentry-session-toggle
matthewwalsh0 Jul 23, 2024
77f847f
Add unit tests
matthewwalsh0 Jul 23, 2024
da5f6ed
Fix linting
matthewwalsh0 Jul 23, 2024
deaa4f0
Add DSN to flask and MMI builds
matthewwalsh0 Jul 24, 2024
5fc0268
Update MMI tests
matthewwalsh0 Jul 24, 2024
9dfbdc4
Merge branch 'develop' into chore/sentry-upgrade-8
matthewwalsh0 Jul 24, 2024
2183e8d
Merge branch 'chore/sentry-upgrade-8' into fix/sentry-session-toggle
matthewwalsh0 Jul 24, 2024
243ae61
Merge branch 'fix/sentry-session-toggle' into refactor/sentry-perform…
matthewwalsh0 Jul 24, 2024
25a9a3f
Handle error in callback
matthewwalsh0 Jul 25, 2024
af88040
Update sample config
matthewwalsh0 Jul 25, 2024
593df42
Merge branch 'develop' into fix/sentry-session-toggle
matthewwalsh0 Jul 29, 2024
67c66b0
Use globalThis
matthewwalsh0 Jul 29, 2024
c65a4d5
Use custom transport
matthewwalsh0 Jul 30, 2024
11290c4
Fix unit tests
matthewwalsh0 Jul 30, 2024
a081498
Fix E2E tests
matthewwalsh0 Jul 30, 2024
d6d4112
Merge branch 'develop' into fix/sentry-session-toggle
matthewwalsh0 Jul 30, 2024
b52bd00
Disable Sentry until onboarding completed
matthewwalsh0 Jul 30, 2024
4ce90c0
Merge branch 'fix/sentry-session-toggle' into refactor/sentry-perform…
matthewwalsh0 Jul 31, 2024
7a40a5c
Fix trace
matthewwalsh0 Jul 31, 2024
698c274
Merge branch 'develop' into refactor/sentry-performance-infrastructure
matthewwalsh0 Jul 31, 2024
713a0ff
Fix unit tests
matthewwalsh0 Jul 31, 2024
cd81e74
Fix unit tests
matthewwalsh0 Jul 31, 2024
3e09194
Fix E2E tests
matthewwalsh0 Jul 31, 2024
8389203
Add messages for Sentry buttons
matthewwalsh0 Aug 1, 2024
f1895b6
Fix unit tests
matthewwalsh0 Aug 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions app/_locales/en/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 31 additions & 10 deletions app/scripts/lib/setupSentry.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { filterEvents } from './sentry-filter-events';

const projectLogger = createProjectLogger('sentry');

const log = createModuleLogger(
export const log = createModuleLogger(
projectLogger,
globalThis.document ? 'ui' : 'background',
);

const internalLog = createModuleLogger(log, 'internal');

/* eslint-disable prefer-destructuring */
// Destructuring breaks the inlining of the environment variables
const METAMASK_BUILD_TYPE = process.env.METAMASK_BUILD_TYPE;
Expand Down Expand Up @@ -466,6 +468,7 @@ export default function setupSentry() {

return {
...Sentry,
getMetaMetricsEnabled,
};
}

Expand All @@ -482,6 +485,7 @@ function getClientOptions() {
integrations: [
Sentry.dedupeIntegration(),
Sentry.extraErrorDataIntegration(),
Sentry.browserTracingIntegration(),
filterEvents({ getMetaMetricsEnabled, log }),
],
release: RELEASE,
Expand All @@ -492,6 +496,7 @@ function getClientOptions() {
// we can safely turn them off by setting the `sendClientReports` option to
// `false`.
sendClientReports: false,
tracesSampleRate: 0.01,
transport: makeTransport,
};
}
Expand Down Expand Up @@ -648,6 +653,7 @@ function setSentryClient() {
release,
});

Sentry.registerSpanErrorInstrumentation();
Sentry.init(clientOptions);

addDebugListeners();
Expand Down Expand Up @@ -872,7 +878,7 @@ function integrateLogging() {
for (const loggerType of ['log', 'error']) {
logger[loggerType] = (...args) => {
const message = args[0].replace(`Sentry Logger [${loggerType}]: `, '');
log(message, ...args.slice(1));
internalLog(message, ...args.slice(1));
};
}

Expand All @@ -887,18 +893,14 @@ function addDebugListeners() {
const client = Sentry.getClient();

client?.on('beforeEnvelope', (event) => {
const type = event?.[1]?.[0]?.[0]?.type;
const data = event?.[1]?.[0]?.[1] ?? {};

if (type !== 'session' || data.status !== 'exited') {
return;
if (isCompletedSessionEnvelope(event)) {
log('Completed session', event);
}

log('Completed session', data);
});

client?.on('afterSendEvent', (event) => {
log('Event', event);
const type = getEventType(event);
log(type, event);
});

log('Added debug listeners');
Expand All @@ -915,3 +917,22 @@ function makeTransport(options) {
return await fetch(...args);
});
}

function isCompletedSessionEnvelope(envelope) {
const type = envelope?.[1]?.[0]?.[0]?.type;
const data = envelope?.[1]?.[0]?.[1] ?? {};

return type === 'session' && data.status === 'exited';
}

function getEventType(event) {
if (event.type === 'transaction') {
return 'Trace';
}

if (event.level === 'error') {
return 'Error';
}

return 'Event';
}
37 changes: 10 additions & 27 deletions development/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,39 +55,22 @@ You can inspect the requests in the `Network` tab of your browser's Developer To
by filtering for `POST` requests to `/v1/batch`. The full url will be `http://localhost:9090/v1/batch`
or `https://api.segment.io/v1/batch` respectively.

## Sentry
## Debugging Sentry

### Debugging in Sentry
1. Set `SENTRY_DSN_DEV`, or `SENTRY_DSN` if using a production build, in `.metamaskrc` to a suitable Sentry URL.
- The example value specified in `.metamaskrc.dist` uses the `test-metamask` project in the MetaMask account.
- Alternatively, create a free Sentry account with a new organization and project.
- The DSN is specified in: `Settings > Projects > [Project Name] > Client Keys (DSN)`.

To debug in a production Sentry environment:
2. To display Sentry logs, include `DEBUG=metamask:sentry:*` in `.metamaskrc`.

- If you have not already got a Sentry account, you can create a free account on [Sentry](https://sentry.io/)
- Create a New Sentry Organization
- If you already have an existing Sentry account and workspace, open the sidebar drop down menu, then click `Switch organization` followed by `Create a new organization`
- Create a New Project
- Copy the `Public Key` and `Project ID` from the Client Keys section under your projects Settings
- Select `Settings` in the sidebar menu, then select `Projects` in the secondary menu. Click your project then select `Client Keys (DSN)` from the secondary menu. Click the `Configure` button on the `Client Keys` page and copy your `Project Id` and `Public Key`
- Add/replace the `SENTRY_DSN` and `SENTRY_DSN_DEV` variables in `.metamaskrc`
```
SENTRY_DSN_DEV=https://{SENTRY_PUBLIC_KEY}@sentry.io/{SENTRY_PROJECT_ID}
SENTRY_DSN=https://{SENTRY_PUBLIC_KEY}@sentry.io/{SENTRY_PROJECT_ID}
DDDDDanica marked this conversation as resolved.
Show resolved Hide resolved
```
- Build the project to the `./dist/` folder with `yarn dist`
3. To display more verbose logs if not in a developer build, include `METAMASK_DEBUG=true` in `.metamaskrc`.

Errors reported whilst using the extension will be displayed in Sentry's `Issues` page.
4. Ensure metrics are enabled during onboarding or via `Settings > Security & privacy > Participate in MetaMetrics`.

To debug in test build we need to comment out the below:- <br>
- `setupSentry` function comment the return statement in the `app/scripts/lib
/setupSentry.js` https://github.com/MetaMask/metamask-extension/blob/e3c76ca699e94bacfc43793d28291fa5ddf06752/app/scripts/lib/setupSentry.js#L496
- `setupStateHooks` function set the if condition to true in the `ui
/index.js` https://github.com/MetaMask/metamask-extension/blob/e3c76ca699e94bacfc43793d28291fa5ddf06752/ui/index.js#L242
5. To test Sentry via the developer options in the UI, include `ENABLE_SETTINGS_PAGE_DEV_OPTIONS=true` in `.metamaskrc`.

How to trigger Sentry error:-
1. Open the background console.
2. Load the extension app and open the developer console.
3. Toggle the `Participate in MetaMetrics` menu option to the `ON` position.
DDDDDanica marked this conversation as resolved.
Show resolved Hide resolved
4. Enter `window.stateHooks.throwTestBackgroundError()` into the developer console.
5. There should now be requests sent to sentry in the background network tab.
6. Alternatively, call `window.stateHooks.throwTestError()` or `window.stateHooks.throwTestBackgroundError()` via the UI console.

## Source Maps

Expand Down
112 changes: 112 additions & 0 deletions shared/lib/trace.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Span, startSpan, withIsolationScope } from '@sentry/browser';
import { trace } from './trace';

jest.mock('@sentry/browser', () => ({
withIsolationScope: jest.fn(),
startSpan: jest.fn(),
}));

const NAME_MOCK = 'testTransaction';
const PARENT_CONTEXT_MOCK = {} as Span;

const TAGS_MOCK = {
tag1: 'value1',
tag2: true,
tag3: 123,
};

const DATA_MOCK = {
data1: 'value1',
data2: true,
data3: 123,
};

function mockGetMetaMetricsEnabled(enabled: boolean) {
global.sentry = {
getMetaMetricsEnabled: () => Promise.resolve(enabled),
};
}

describe('Trace', () => {
const startSpanMock = jest.mocked(startSpan);
const withIsolationScopeMock = jest.mocked(withIsolationScope);
const setTagsMock = jest.fn();

beforeEach(() => {
jest.resetAllMocks();

startSpanMock.mockImplementation((_, fn) => fn({} as Span));

// eslint-disable-next-line @typescript-eslint/no-explicit-any
withIsolationScopeMock.mockImplementation((fn: any) =>
fn({ setTags: setTagsMock }),
);
});

describe('trace', () => {
// @ts-expect-error This function is missing from the Mocha type definitions
matthewwalsh0 marked this conversation as resolved.
Show resolved Hide resolved
it.each([
['enabled', true],
['disabled', false],
])(
'executes callback if Sentry is %s',
async (_: string, sentryEnabled: boolean) => {
let callbackExecuted = false;

mockGetMetaMetricsEnabled(sentryEnabled);

await trace({ name: NAME_MOCK }, async () => {
callbackExecuted = true;
});

expect(callbackExecuted).toBe(true);
},
);

// @ts-expect-error This function is missing from the Mocha type definitions
it.each([
['enabled', true],
['disabled', false],
])(
'returns value from callback if Sentry is %s',
async (_: string, sentryEnabled: boolean) => {
mockGetMetaMetricsEnabled(sentryEnabled);

const result = await trace({ name: NAME_MOCK }, async () => {
return true;
});

expect(result).toBe(true);
},
);

it('invokes Sentry if enabled', async () => {
mockGetMetaMetricsEnabled(true);

await trace(
{
name: NAME_MOCK,
tags: TAGS_MOCK,
data: DATA_MOCK,
parentContext: PARENT_CONTEXT_MOCK,
},
async () => Promise.resolve(),
);

expect(withIsolationScopeMock).toHaveBeenCalledTimes(1);

expect(startSpanMock).toHaveBeenCalledTimes(1);
expect(startSpanMock).toHaveBeenCalledWith(
{
name: NAME_MOCK,
parentSpan: PARENT_CONTEXT_MOCK,
attributes: DATA_MOCK,
},
expect.any(Function),
);

expect(setTagsMock).toHaveBeenCalledTimes(1);
expect(setTagsMock).toHaveBeenCalledWith(TAGS_MOCK);
});
});
});
54 changes: 54 additions & 0 deletions shared/lib/trace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as Sentry from '@sentry/browser';
import { Primitive } from '@sentry/types';
import { createModuleLogger } from '@metamask/utils';
import { log as sentryLogger } from '../../app/scripts/lib/setupSentry';

const log = createModuleLogger(sentryLogger, 'trace');

export type TraceRequest = {
data?: Record<string, number | string | boolean>;
name: string;
parentContext?: unknown;
tags?: Record<string, number | string | boolean>;
};

export async function trace<T>(
request: TraceRequest,
fn: (context?: unknown) => Promise<T>,
): Promise<T> {
const { data: attributes, name, parentContext, tags } = request;
const parentSpan = (parentContext ?? null) as Sentry.Span | null;

const isSentryEnabled =
(await globalThis.sentry.getMetaMetricsEnabled()) as boolean;

const callback = async (span: Sentry.Span | null) => {
log('Starting trace', name, request);

const start = Date.now();
let error;

try {
return await fn(span);
} catch (currentError) {
error = currentError;
throw currentError;
} finally {
const end = Date.now();
const duration = end - start;

log('Finished trace', name, duration, { error, request });
}
};

if (!isSentryEnabled) {
log('Skipping Sentry trace as metrics disabled', name, request);
return callback(null);
}

return await Sentry.withIsolationScope(async (scope) => {
scope.setTags(tags as Record<string, Primitive>);

return await Sentry.startSpan({ name, parentSpan, attributes }, callback);
});
}
27 changes: 26 additions & 1 deletion types/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Many of the state hooks return untyped raw state.
/* eslint-disable @typescript-eslint/no-explicit-any */

// 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
/* eslint-disable no-var */

import * as Sentry from '@sentry/browser';
import {
Success,
Expand Down Expand Up @@ -224,13 +228,32 @@ declare class Chrome {
runtime: Runtime;
}

type SentryObject = Sentry;
type SentryObject = Sentry & {
getMetaMetricsEnabled: () => Promise<boolean>;
};

type HttpProvider = {
host: string;
timeout: number;
};

type StateHooks = {
getCleanAppState?: () => Promise<any>;
getLogs?: () => any[];
getMostRecentPersistedState?: () => any;
getPersistedState: () => Promise<any>;
getSentryAppState?: () => any;
DDDDDanica marked this conversation as resolved.
Show resolved Hide resolved
getSentryState: () => {
browser: string;
version: string;
state?: any;
persistedState?: any;
};
metamaskGetState?: () => Promise<any>;
throwTestBackgroundError?: (msg?: string) => Promise<void>;
throwTestError?: (msg?: string) => void;
};

export declare global {
var platform: Platform;
// Sentry is undefined in dev, so use optional chaining
Expand All @@ -240,6 +263,8 @@ export declare global {

var ethereumProvider: HttpProvider;

var stateHooks: StateHooks;

namespace jest {
// The interface is being used for declaration merging, which is an acceptable exception to this rule.
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
Expand Down
Loading