Skip to content

Commit

Permalink
feat: Add support for performance metrics (#811)
Browse files Browse the repository at this point in the history
  • Loading branch information
timfish committed Jan 8, 2024
1 parent 3e5a2c0 commit 1bf073f
Show file tree
Hide file tree
Showing 16 changed files with 223 additions and 11 deletions.
14 changes: 14 additions & 0 deletions src/common/ipc.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { MeasurementUnit, Primitive } from '@sentry/types';

export const PROTOCOL_SCHEME = 'sentry-ipc';

export enum IPCChannel {
Expand All @@ -11,6 +13,8 @@ export enum IPCChannel {
ENVELOPE = 'sentry-electron.envelope',
/** IPC to pass renderer status updates */
STATUS = 'sentry-electron.status',
/** IPC to pass renderer metric additions to the main process */
ADD_METRIC = 'sentry-electron.add-metric',
}

export interface RendererProcessAnrOptions {
Expand Down Expand Up @@ -39,12 +43,22 @@ export interface RendererStatus {
config: RendererProcessAnrOptions;
}

export interface MetricIPCMessage {
metricType: 'c' | 'g' | 's' | 'd';
name: string;
value: number | string;
unit?: MeasurementUnit;
tags?: Record<string, Primitive>;
timestamp?: number;
}

export interface IPCInterface {
sendRendererStart: () => void;
sendScope: (scope: string) => void;
sendEvent: (event: string) => void;
sendEnvelope: (evn: Uint8Array | string) => void;
sendStatus: (state: RendererStatus) => void;
sendAddMetric: (metric: MetricIPCMessage) => void;
}

export const RENDERER_ID_HEADER = 'sentry-electron-renderer-id';
Expand Down
39 changes: 36 additions & 3 deletions src/main/ipc.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { captureEvent, getCurrentHub, getCurrentScope } from '@sentry/core';
import { Attachment, AttachmentItem, Envelope, Event, EventItem, Profile, ScopeData } from '@sentry/types';
import { BaseClient, captureEvent, getClient, getCurrentScope } from '@sentry/core';
import {
Attachment,
AttachmentItem,
ClientOptions,
Envelope,
Event,
EventItem,
Profile,
ScopeData,
} from '@sentry/types';
import { forEachEnvelopeItem, logger, parseEnvelope, SentryError } from '@sentry/utils';
import { app, ipcMain, protocol, WebContents, webContents } from 'electron';
import { TextDecoder, TextEncoder } from 'util';
Expand All @@ -8,6 +17,7 @@ import {
IPCChannel,
IPCMode,
mergeEvents,
MetricIPCMessage,
normalizeUrlsInReplayEnvelope,
PROTOCOL_SCHEME,
RendererStatus,
Expand Down Expand Up @@ -127,7 +137,26 @@ function handleEnvelope(options: ElectronMainOptionsInternal, env: Uint8Array |
} else {
const normalizedEnvelope = normalizeUrlsInReplayEnvelope(envelope, app.getAppPath());
// Pass other types of envelope straight to the transport
void getCurrentHub().getClient()?.getTransport()?.send(normalizedEnvelope);
void getClient()?.getTransport()?.send(normalizedEnvelope);
}
}

function handleMetric(metric: MetricIPCMessage): void {
const client = getClient<BaseClient<ClientOptions>>();

if (client?.metricsAggregator) {
client.metricsAggregator.add(
metric.metricType,
metric.name,
metric.value,
metric.unit,
metric.tags,
metric.timestamp,
);
} else {
logger.warn(
`Metric was dropped because the aggregator is not configured in the main process. Enable via '_experiments.metricsAggregator: true' in your init call.`,
);
}
}

Expand Down Expand Up @@ -208,6 +237,8 @@ function configureProtocol(options: ElectronMainOptionsInternal): void {
handleScope(options, data.toString());
} else if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.ENVELOPE}`) && data) {
handleEnvelope(options, data, getWebContents());
} else if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.ADD_METRIC}`) && data) {
handleMetric(JSON.parse(data.toString()) as MetricIPCMessage);
} else if (request.url.startsWith(`${PROTOCOL_SCHEME}://${IPCChannel.STATUS}`) && data) {
const contents = getWebContents();
if (contents) {
Expand Down Expand Up @@ -245,6 +276,8 @@ function configureClassic(options: ElectronMainOptionsInternal): void {

const rendererStatusChanged = createRendererAnrStatusHandler();
ipcMain.on(IPCChannel.STATUS, ({ sender }, status: RendererStatus) => rendererStatusChanged(status, sender));

ipcMain.on(IPCChannel.ADD_METRIC, (_, metric: MetricIPCMessage) => handleMetric(metric));
}

/** Sets up communication channels with the renderer */
Expand Down
3 changes: 2 additions & 1 deletion src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { contextBridge, ipcRenderer } from 'electron';

import { IPCChannel, RendererStatus } from '../common/ipc';
import { IPCChannel, MetricIPCMessage, RendererStatus } from '../common/ipc';

// eslint-disable-next-line no-restricted-globals
if (window.__SENTRY_IPC__) {
Expand All @@ -17,6 +17,7 @@ if (window.__SENTRY_IPC__) {
sendEvent: (eventJson: string) => ipcRenderer.send(IPCChannel.EVENT, eventJson),
sendEnvelope: (envelope: Uint8Array | string) => ipcRenderer.send(IPCChannel.ENVELOPE, envelope),
sendStatus: (status: RendererStatus) => ipcRenderer.send(IPCChannel.STATUS, status),
sendAddMetric: (metric: MetricIPCMessage) => ipcRenderer.send(IPCChannel.ADD_METRIC, metric),
};

// eslint-disable-next-line no-restricted-globals
Expand Down
3 changes: 2 additions & 1 deletion src/preload/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { contextBridge, crashReporter, ipcRenderer } from 'electron';
import * as electron from 'electron';

import { IPCChannel, RendererStatus } from '../common/ipc';
import { IPCChannel, MetricIPCMessage, RendererStatus } from '../common/ipc';

// eslint-disable-next-line no-restricted-globals
if (window.__SENTRY_IPC__) {
Expand All @@ -28,6 +28,7 @@ if (window.__SENTRY_IPC__) {
sendEvent: (eventJson: string) => ipcRenderer.send(IPCChannel.EVENT, eventJson),
sendEnvelope: (envelope: Uint8Array | string) => ipcRenderer.send(IPCChannel.ENVELOPE, envelope),
sendStatus: (status: RendererStatus) => ipcRenderer.send(IPCChannel.STATUS, status),
sendAddMetric: (metric: MetricIPCMessage) => ipcRenderer.send(IPCChannel.ADD_METRIC, metric),
};

// eslint-disable-next-line no-restricted-globals
Expand Down
13 changes: 11 additions & 2 deletions src/renderer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,19 @@ export {
startInactiveSpan,
startSpanManual,
continueTrace,
metrics,
} from '@sentry/core';
export type { SpanStatusType } from '@sentry/core';

import { metrics as coreMetrics } from '@sentry/core';

import { MetricsAggregator } from './integrations/metrics-aggregator';

export const metrics = {
...coreMetrics,
// Override the default browser metrics aggregator with the Electron renderer one
MetricsAggregator,
};

export {
addTracingExtensions,
BrowserClient,
Expand All @@ -86,5 +95,5 @@ export {
// eslint-disable-next-line deprecation/deprecation
export type { BrowserOptions, ReportDialogOptions } from '@sentry/browser';

export const Integrations = { ...ElectronRendererIntegrations, ...BrowserIntegrations };
export const Integrations = { ...BrowserIntegrations, ...ElectronRendererIntegrations };
export { init, defaultIntegrations } from './sdk';
24 changes: 24 additions & 0 deletions src/renderer/integrations/metrics-aggregator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { BrowserClient } from '@sentry/browser';
import { convertIntegrationFnToClass } from '@sentry/core';
import type { IntegrationFn } from '@sentry/types';

import { ElectronRendererMetricsAggregator } from '../metrics';

const INTEGRATION_NAME = 'MetricsAggregator';

const metricsAggregatorIntegration: IntegrationFn = () => {
return {
name: INTEGRATION_NAME,
setup(client: BrowserClient) {
client.metricsAggregator = new ElectronRendererMetricsAggregator();
},
};
};

/**
* Enables Sentry metrics monitoring.
*
* @experimental This API is experimental and might having breaking changes in the future.
*/
// eslint-disable-next-line deprecation/deprecation
export const MetricsAggregator = convertIntegrationFnToClass(INTEGRATION_NAME, metricsAggregatorIntegration);
14 changes: 13 additions & 1 deletion src/renderer/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
/* eslint-disable no-console */
import { logger, uuid4 } from '@sentry/utils';

import { IPCChannel, IPCInterface, PROTOCOL_SCHEME, RENDERER_ID_HEADER, RendererStatus } from '../common/ipc';
import {
IPCChannel,
IPCInterface,
MetricIPCMessage,
PROTOCOL_SCHEME,
RENDERER_ID_HEADER,
RendererStatus,
} from '../common/ipc';

function buildUrl(channel: IPCChannel): string {
// We include sentry_key in the URL so these don't end up in fetch breadcrumbs
Expand Down Expand Up @@ -52,6 +59,11 @@ function getImplementation(): IPCInterface {
// ignore
});
},
sendAddMetric: (metric: MetricIPCMessage) => {
fetch(buildUrl(IPCChannel.ADD_METRIC), { method: 'POST', body: JSON.stringify(metric), headers }).catch(() => {
// ignore
});
},
};
}
}
Expand Down
42 changes: 42 additions & 0 deletions src/renderer/metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { MeasurementUnit, MetricsAggregator, Primitive } from '@sentry/types';

import { IPCInterface } from '../common/ipc';
import { getIPC } from './ipc';

/**
* Sends metrics to the Electron main process where they can be aggregated in a single process
*/
export class ElectronRendererMetricsAggregator implements MetricsAggregator {
private readonly _ipc: IPCInterface;

public constructor() {
this._ipc = getIPC();
}

/** @inheritdoc */
public add(
metricType: 'c' | 'g' | 's' | 'd',
name: string,
value: string | number,
unit?: MeasurementUnit | undefined,
tags?: Record<string, Primitive> | undefined,
timestamp?: number | undefined,
): void {
this._ipc.sendAddMetric({ metricType, name, value, unit, tags, timestamp });
}

/** @inheritdoc */
public flush(): void {
// do nothing
}

/** @inheritdoc */
public close(): void {
// do nothing
}

/** @inheritdoc */
public toString(): string {
return '';
}
}
3 changes: 2 additions & 1 deletion src/renderer/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import { logger } from '@sentry/utils';
import { ensureProcess, RendererProcessAnrOptions } from '../common';
import { enableAnrRendererMessages } from './anr';
import { ScopeToMain } from './integrations';
import { MetricsAggregator } from './integrations/metrics-aggregator';
import { electronRendererStackParser } from './stack-parse';
import { makeRendererTransport } from './transport';

export const defaultIntegrations = [...defaultBrowserIntegrations, new ScopeToMain()];
export const defaultIntegrations = [...defaultBrowserIntegrations, new ScopeToMain(), new MetricsAggregator()];

interface ElectronRendererOptions extends BrowserOptions {
/**
Expand Down
4 changes: 4 additions & 0 deletions test/e2e/recipe/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export function normalize(event: TestServerEvent<Event | Transaction | Session>)
}

normalizeProfile(event.profile);

if (event.metrics) {
event.metrics = event.metrics.replace(/T\d{1,10}\n/g, 'T0000000000\n');
}
}

export function eventIsSession(data: EventOrSession): boolean {
Expand Down
12 changes: 10 additions & 2 deletions test/e2e/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export interface TestServerEvent<T = unknown> {
attachments?: Attachment[];
/** Profiling data */
profile?: Profile;
/** Metrics data */
metrics?: string;
/** API method used for submission */
method: 'envelope' | 'minidump' | 'store';
}
Expand Down Expand Up @@ -124,6 +126,7 @@ export class TestServer {
let data: Event | Transaction | Session | ReplayEvent | undefined;
const attachments: Attachment[] = [];
let profile: Profile | undefined;
let metrics: string | undefined;

forEachEnvelopeItem(envelope, ([headers, item]) => {
if (headers.type === 'event' || headers.type === 'transaction' || headers.type === 'session') {
Expand All @@ -142,16 +145,21 @@ export class TestServer {
attachments.push(headers);
}

if (headers.type === 'statsd') {
metrics = item.toString();
}

if (headers.type === 'profile') {
profile = item as unknown as Profile;
}
});

if (data) {
if (data || metrics) {
this._addEvent({
data,
data: data || {},
attachments,
profile,
metrics,
appId: ctx.params.id,
sentryKey: keyMatch[1],
method: 'envelope',
Expand Down
6 changes: 6 additions & 0 deletions test/e2e/test-apps/other/metrics/event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"method": "envelope",
"sentryKey": "37f8a2ee37c0409d8970bc7559c7c7e4",
"appId": "277345",
"metrics": "parallel_requests@none:2:2:2:2:1|g|#release:metrics@1.0.0,environment:development,type:a|T0000000000\nhits@none:4|c|T0000000000\n"
}
8 changes: 8 additions & 0 deletions test/e2e/test-apps/other/metrics/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "metrics",
"version": "1.0.0",
"main": "src/main.js",
"dependencies": {
"@sentry/electron": "3.0.0"
}
}
3 changes: 3 additions & 0 deletions test/e2e/test-apps/other/metrics/recipe.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
description: Metrics
command: yarn

14 changes: 14 additions & 0 deletions test/e2e/test-apps/other/metrics/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<script>
const Sentry = require('@sentry/electron/renderer');

Sentry.init();
Sentry.metrics.increment('hits', 4);
</script>
</body>
</html>
32 changes: 32 additions & 0 deletions test/e2e/test-apps/other/metrics/src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const path = require('path');

const { app, BrowserWindow } = require('electron');
const Sentry = require('@sentry/electron');

Sentry.init({
dsn: '__DSN__',
debug: true,
autoSessionTracking: false,
onFatalError: () => {},
_experiments: {
metricsAggregator: true,
},
});

Sentry.metrics.gauge('parallel_requests', 2, { tags: { type: 'a' } });

app.on('ready', () => {
const mainWindow = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});

mainWindow.loadFile(path.join(__dirname, 'index.html'));

setTimeout(() => {
Sentry.flush(2000);
}, 2000);
});

0 comments on commit 1bf073f

Please sign in to comment.