Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions src/common/ipc.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SerializedLog } from '@sentry/core';
import { SerializedLog, SerializedMetric } from '@sentry/core';

/** Ways to communicate between the renderer and main process */
export enum IPCMode {
Expand All @@ -25,7 +25,9 @@ export type Channel =
/** IPC to pass renderer status updates */
| 'status'
/** IPC to pass structured log messages */
| 'structured-log';
| 'structured-log'
/** IPC to pass metric data */
| 'metric';

export interface IpcUtils {
createUrl: (channel: Channel) => string;
Expand Down Expand Up @@ -85,6 +87,7 @@ export interface IPCInterface {
sendEnvelope: (evn: Uint8Array | string) => void;
sendStatus: (state: RendererStatus) => void;
sendStructuredLog: (log: SerializedLog) => void;
sendMetric: (metric: SerializedMetric) => void;
}

export const RENDERER_ID_HEADER = 'sentry-electron-renderer-id';
Expand Down
57 changes: 42 additions & 15 deletions src/main/ipc.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import {
type SerializedLog,
type SerializedMetric,
_INTERNAL_captureSerializedLog,
_INTERNAL_captureSerializedMetric,
Attachment,
Client,
debug,
DynamicSamplingContext,
Event,
parseEnvelope,
ScopeData,
SerializedLog,
} from '@sentry/core';
import { captureEvent, getClient, getCurrentScope } from '@sentry/node';
import { app, ipcMain, protocol, WebContents, webContents } from 'electron';
Expand Down Expand Up @@ -161,50 +163,70 @@ function handleScope(options: ElectronMainOptionsInternal, jsonScope: string): v
}
}

function handleLogFromRenderer(
function handleAttributes(
client: Client,
options: ElectronMainOptionsInternal,
log: SerializedLog,
contents: WebContents | undefined,
): void {
maybeAttributes?: SerializedLog['attributes'],
): SerializedLog['attributes'] {
const process = contents ? options?.getRendererName?.(contents) || 'renderer' : 'renderer';

log.attributes = log.attributes || {};
const attributes: SerializedLog['attributes'] = maybeAttributes || {};

if (options.release) {
log.attributes['sentry.release'] = { value: options.release, type: 'string' };
attributes['sentry.release'] = { value: options.release, type: 'string' };
}

if (options.environment) {
log.attributes['sentry.environment'] = { value: options.environment, type: 'string' };
attributes['sentry.environment'] = { value: options.environment, type: 'string' };
}

log.attributes['sentry.sdk.name'] = { value: 'sentry.javascript.electron', type: 'string' };
log.attributes['sentry.sdk.version'] = { value: SDK_VERSION, type: 'string' };
attributes['sentry.sdk.name'] = { value: 'sentry.javascript.electron', type: 'string' };
attributes['sentry.sdk.version'] = { value: SDK_VERSION, type: 'string' };

log.attributes['electron.process'] = { value: process, type: 'string' };
attributes['electron.process'] = { value: process, type: 'string' };

const osDeviceAttributes = getOsDeviceLogAttributes(client);

if (osDeviceAttributes['os.name']) {
log.attributes['os.name'] = { value: osDeviceAttributes['os.name'], type: 'string' };
attributes['os.name'] = { value: osDeviceAttributes['os.name'], type: 'string' };
}
if (osDeviceAttributes['os.version']) {
log.attributes['os.version'] = { value: osDeviceAttributes['os.version'], type: 'string' };
attributes['os.version'] = { value: osDeviceAttributes['os.version'], type: 'string' };
}
if (osDeviceAttributes['device.brand']) {
log.attributes['device.brand'] = { value: osDeviceAttributes['device.brand'], type: 'string' };
attributes['device.brand'] = { value: osDeviceAttributes['device.brand'], type: 'string' };
}
if (osDeviceAttributes['device.model']) {
log.attributes['device.model'] = { value: osDeviceAttributes['device.model'], type: 'string' };
attributes['device.model'] = { value: osDeviceAttributes['device.model'], type: 'string' };
}
if (osDeviceAttributes['device.family']) {
log.attributes['device.family'] = { value: osDeviceAttributes['device.family'], type: 'string' };
attributes['device.family'] = { value: osDeviceAttributes['device.family'], type: 'string' };
}

return attributes;
}

function handleLogFromRenderer(
client: Client,
options: ElectronMainOptionsInternal,
log: SerializedLog,
contents: WebContents | undefined,
): void {
log.attributes = handleAttributes(client, options, contents, log.attributes);
_INTERNAL_captureSerializedLog(client, log);
}

function handleMetricFromRenderer(
client: Client,
options: ElectronMainOptionsInternal,
metric: SerializedMetric,
contents: WebContents | undefined,
): void {
metric.attributes = handleAttributes(client, options, contents, metric.attributes);
_INTERNAL_captureSerializedMetric(client, metric);
}

/** Enables Electron protocol handling */
function configureProtocol(client: Client, ipcUtil: IpcUtils, options: ElectronMainOptionsInternal): void {
if (app.isReady()) {
Expand Down Expand Up @@ -247,6 +269,8 @@ function configureProtocol(client: Client, ipcUtil: IpcUtils, options: ElectronM
handleEnvelope(client, options, data, getWebContents());
} else if (ipcUtil.urlMatches(request.url, 'structured-log') && data) {
handleLogFromRenderer(client, options, JSON.parse(data.toString()), getWebContents());
} else if (ipcUtil.urlMatches(request.url, 'metric') && data) {
handleMetricFromRenderer(client, options, JSON.parse(data.toString()), getWebContents());
} else if (rendererStatusChanged && ipcUtil.urlMatches(request.url, 'status') && data) {
const contents = getWebContents();
if (contents) {
Expand Down Expand Up @@ -289,6 +313,9 @@ function configureClassic(client: Client, ipcUtil: IpcUtils, options: ElectronMa
ipcMain.on(ipcUtil.createKey('structured-log'), ({ sender }, log: SerializedLog) =>
handleLogFromRenderer(client, options, log, sender),
);
ipcMain.on(ipcUtil.createKey('metric'), ({ sender }, metric: SerializedMetric) =>
handleMetricFromRenderer(client, options, metric, sender),
);

const rendererStatusChanged = createRendererEventLoopBlockStatusHandler(client);
if (rendererStatusChanged) {
Expand Down
3 changes: 2 additions & 1 deletion src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* This preload script may be used with sandbox mode enabled which means regular require is not available.
*/

import { SerializedLog } from '@sentry/core';
import { SerializedLog, SerializedMetric } from '@sentry/core';
import { contextBridge, ipcRenderer } from 'electron';
import { ipcChannelUtils, RendererStatus } from '../common/ipc.js';

Expand All @@ -28,6 +28,7 @@ export function hookupIpc(namespace: string = 'sentry-ipc'): void {
sendEnvelope: (envelope: Uint8Array | string) => ipcRenderer.send(ipcUtil.createKey('envelope'), envelope),
sendStatus: (status: RendererStatus) => ipcRenderer.send(ipcUtil.createKey('status'), status),
sendStructuredLog: (log: SerializedLog) => ipcRenderer.send(ipcUtil.createKey('structured-log'), log),
sendMetric: (metric: SerializedMetric) => ipcRenderer.send(ipcUtil.createKey('metric'), metric),
};

// eslint-disable-next-line no-restricted-globals
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as logger from './log.js';
import * as metrics from './metric.js';

export { logger };
export { logger, metrics };

export type {
Breadcrumb,
Expand Down Expand Up @@ -84,7 +85,6 @@ export {
lastEventId,
launchDarklyIntegration,
linkedErrorsIntegration,
metrics,
moduleMetadataIntegration,
onLoad,
openFeatureIntegration,
Expand Down
11 changes: 10 additions & 1 deletion src/renderer/ipc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-globals */
/* eslint-disable no-console */
import { Client, debug, getClient, SerializedLog, uuid4 } from '@sentry/core';
import { Client, debug, getClient, SerializedLog, SerializedMetric, uuid4 } from '@sentry/core';
import { ipcChannelUtils, IPCInterface, RENDERER_ID_HEADER, RendererStatus } from '../common/ipc.js';
import { ElectronRendererOptionsInternal } from './sdk.js';

Expand Down Expand Up @@ -56,6 +56,15 @@ function getImplementation(ipcKey: string): IPCInterface {
// ignore
});
},
sendMetric: (metric: SerializedMetric) => {
fetch(ipcUtil.createUrl('metric'), {
method: 'POST',
body: JSON.stringify(metric),
headers,
}).catch(() => {
// ignore
});
},
};
}
}
Expand Down
122 changes: 122 additions & 0 deletions src/renderer/metric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {
_INTERNAL_captureMetric,
getClient,
getCurrentScope,
MetricOptions,
MetricType,
SerializedMetric,
} from '@sentry/core';
import { getIPC } from './ipc.js';

function captureMetric(type: MetricType, name: string, value: number, options?: MetricOptions): void {
const client = getClient();
_INTERNAL_captureMetric(
{ type, name, value, unit: options?.unit, attributes: options?.attributes },
{
scope: options?.scope ?? getCurrentScope(),
captureSerializedMetric: (_: unknown, metric: SerializedMetric) => getIPC(client).sendMetric(metric),
},
);
}

/**
* @summary Increment a counter metric. Requires the `_experiments.enableMetrics` option to be enabled.
*
* @param name - The name of the counter metric.
* @param value - The value to increment by (defaults to 1).
* @param options - Options for capturing the metric.
*
* @example
*
* ```
* Sentry.metrics.count('api.requests', 1, {
* attributes: {
* endpoint: '/api/users',
* method: 'GET',
* status: 200
* }
* });
* ```
*
* @example With custom value
*
* ```
* Sentry.metrics.count('items.processed', 5, {
* attributes: {
* processor: 'batch-processor',
* queue: 'high-priority'
* }
* });
* ```
*/
export function count(name: string, value: number = 1, options?: MetricOptions): void {
captureMetric('counter', name, value, options);
}

/**
* @summary Set a gauge metric to a specific value. Requires the `_experiments.enableMetrics` option to be enabled.
*
* @param name - The name of the gauge metric.
* @param value - The current value of the gauge.
* @param options - Options for capturing the metric.
*
* @example
*
* ```
* Sentry.metrics.gauge('memory.usage', 1024, {
* unit: 'megabyte',
* attributes: {
* process: 'web-server',
* region: 'us-east-1'
* }
* });
* ```
*
* @example Without unit
*
* ```
* Sentry.metrics.gauge('active.connections', 42, {
* attributes: {
* server: 'api-1',
* protocol: 'websocket'
* }
* });
* ```
*/
export function gauge(name: string, value: number, options?: MetricOptions): void {
captureMetric('gauge', name, value, options);
}

/**
* @summary Record a value in a distribution metric. Requires the `_experiments.enableMetrics` option to be enabled.
*
* @param name - The name of the distribution metric.
* @param value - The value to record in the distribution.
* @param options - Options for capturing the metric.
*
* @example
*
* ```
* Sentry.metrics.distribution('task.duration', 500, {
* unit: 'millisecond',
* attributes: {
* task: 'data-processing',
* priority: 'high'
* }
* });
* ```
*
* @example Without unit
*
* ```
* Sentry.metrics.distribution('batch.size', 100, {
* attributes: {
* processor: 'batch-1',
* type: 'async'
* }
* });
* ```
*/
export function distribution(name: string, value: number, options?: MetricOptions): void {
captureMetric('distribution', name, value, options);
}
9 changes: 9 additions & 0 deletions test/e2e/test-apps/other/metrics/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "electron-metrics",
"description": "Electron Metrics",
"version": "1.0.0",
"main": "src/main.js",
"dependencies": {
"@sentry/electron": "7.2.0"
}
}
24 changes: 24 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,24 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<script>
const { init, metrics } = require('@sentry/electron/renderer');

init({
debug: true,
});

setTimeout(() => {
metrics.count('User clicked submit button', 1, {
attributes: {
buttonId: 'submit-form',
formId: 'user-profile',
},
});
}, 500);
</script>
</body>
</html>
35 changes: 35 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,35 @@
const path = require('path');

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

init({
dsn: '__DSN__',
debug: true,
onFatalError: () => {},
});

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

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

setTimeout(() => {
metrics.count('User profile updated', 1, {
attributes: {
userId: 'user_123',
updatedFields: ['email', 'preferences'],
},
});
}, 500);
});

setTimeout(() => {
app.quit();
}, 4000);
Loading
Loading