Skip to content

Commit 1093760

Browse files
authored
feat: Log os and device attributes (#1246)
1 parent 814d6a6 commit 1093760

File tree

6 files changed

+134
-22
lines changed

6 files changed

+134
-22
lines changed

src/main/ipc.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { ipcChannelUtils, IPCMode, IpcUtils, RendererStatus } from '../common/ip
1616
import { registerProtocol } from './electron-normalize.js';
1717
import { createRendererEventLoopBlockStatusHandler } from './integrations/renderer-anr.js';
1818
import { rendererProfileFromIpc } from './integrations/renderer-profiling.js';
19+
import { getOsDeviceLogAttributes } from './log.js';
1920
import { mergeEvents } from './merge.js';
2021
import { normalizeReplayEnvelope } from './normalize.js';
2122
import { ElectronMainOptionsInternal } from './sdk.js';
@@ -55,7 +56,7 @@ function captureEventFromRenderer(
5556
event: Event,
5657
dynamicSamplingContext: Partial<DynamicSamplingContext> | undefined,
5758
attachments: Attachment[],
58-
contents?: WebContents,
59+
contents: WebContents | undefined,
5960
): void {
6061
const process = contents ? options?.getRendererName?.(contents) || 'renderer' : 'renderer';
6162

@@ -160,7 +161,14 @@ function handleScope(options: ElectronMainOptionsInternal, jsonScope: string): v
160161
}
161162
}
162163

163-
function handleLogFromRenderer(client: Client, options: ElectronMainOptionsInternal, log: SerializedLog): void {
164+
function handleLogFromRenderer(
165+
client: Client,
166+
options: ElectronMainOptionsInternal,
167+
log: SerializedLog,
168+
contents: WebContents | undefined,
169+
): void {
170+
const process = contents ? options?.getRendererName?.(contents) || 'renderer' : 'renderer';
171+
164172
log.attributes = log.attributes || {};
165173

166174
if (options.release) {
@@ -174,6 +182,26 @@ function handleLogFromRenderer(client: Client, options: ElectronMainOptionsInter
174182
log.attributes['sentry.sdk.name'] = { value: 'sentry.javascript.electron', type: 'string' };
175183
log.attributes['sentry.sdk.version'] = { value: SDK_VERSION, type: 'string' };
176184

185+
log.attributes['electron.process'] = { value: process, type: 'string' };
186+
187+
const osDeviceAttributes = getOsDeviceLogAttributes(client);
188+
189+
if (osDeviceAttributes['os.name']) {
190+
log.attributes['os.name'] = { value: osDeviceAttributes['os.name'], type: 'string' };
191+
}
192+
if (osDeviceAttributes['os.version']) {
193+
log.attributes['os.version'] = { value: osDeviceAttributes['os.version'], type: 'string' };
194+
}
195+
if (osDeviceAttributes['device.brand']) {
196+
log.attributes['device.brand'] = { value: osDeviceAttributes['device.brand'], type: 'string' };
197+
}
198+
if (osDeviceAttributes['device.model']) {
199+
log.attributes['device.model'] = { value: osDeviceAttributes['device.model'], type: 'string' };
200+
}
201+
if (osDeviceAttributes['device.family']) {
202+
log.attributes['device.family'] = { value: osDeviceAttributes['device.family'], type: 'string' };
203+
}
204+
177205
_INTERNAL_captureSerializedLog(client, log);
178206
}
179207

@@ -218,7 +246,7 @@ function configureProtocol(client: Client, ipcUtil: IpcUtils, options: ElectronM
218246
} else if (ipcUtil.urlMatches(request.url, 'envelope') && data) {
219247
handleEnvelope(client, options, data, getWebContents());
220248
} else if (ipcUtil.urlMatches(request.url, 'structured-log') && data) {
221-
handleLogFromRenderer(client, options, JSON.parse(data.toString()));
249+
handleLogFromRenderer(client, options, JSON.parse(data.toString()), getWebContents());
222250
} else if (rendererStatusChanged && ipcUtil.urlMatches(request.url, 'status') && data) {
223251
const contents = getWebContents();
224252
if (contents) {
@@ -258,8 +286,8 @@ function configureClassic(client: Client, ipcUtil: IpcUtils, options: ElectronMa
258286
ipcMain.on(ipcUtil.createKey('envelope'), ({ sender }, env: Uint8Array | string) =>
259287
handleEnvelope(client, options, env, sender),
260288
);
261-
ipcMain.on(ipcUtil.createKey('structured-log'), (_, log: SerializedLog) =>
262-
handleLogFromRenderer(client, options, log),
289+
ipcMain.on(ipcUtil.createKey('structured-log'), ({ sender }, log: SerializedLog) =>
290+
handleLogFromRenderer(client, options, log, sender),
263291
);
264292

265293
const rendererStatusChanged = createRendererEventLoopBlockStatusHandler(client);

src/main/log.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Client, Event } from '@sentry/core';
2+
3+
interface Attributes {
4+
'os.name'?: string;
5+
'os.version'?: string;
6+
'device.brand'?: string;
7+
'device.model'?: string;
8+
'device.family'?: string;
9+
}
10+
11+
/**
12+
* Fetch os and device attributes from the Context and AdditionalContext integrations
13+
*/
14+
async function getAttributes(client: Client): Promise<Attributes> {
15+
const contextIntegration = client.getIntegrationByName('Context');
16+
const additionalContextIntegration = client.getIntegrationByName('AdditionalContext');
17+
18+
let event: Event = {};
19+
const hint = {};
20+
21+
event = (await contextIntegration?.processEvent?.(event, hint, client)) || event;
22+
event = (await additionalContextIntegration?.processEvent?.(event, hint, client)) || event;
23+
24+
const attrs: Attributes = {};
25+
if (event.contexts?.os?.name) {
26+
attrs['os.name'] = event.contexts.os.name;
27+
}
28+
if (event.contexts?.os?.version) {
29+
attrs['os.version'] = event.contexts.os.version;
30+
}
31+
if (event.contexts?.device?.brand) {
32+
attrs['device.brand'] = event.contexts.device.brand;
33+
}
34+
if (event.contexts?.device?.model) {
35+
attrs['device.model'] = event.contexts.device.model;
36+
}
37+
if (event.contexts?.device?.family) {
38+
attrs['device.family'] = event.contexts.device.family;
39+
}
40+
return attrs;
41+
}
42+
43+
// Cached attributes
44+
let attributes: Attributes | undefined;
45+
46+
/**
47+
* Get OS and device attributes for logs
48+
*
49+
* Some of this context is only available asynchronously, so we fetch it once
50+
* and cache it for future logs. Logs before the attributes are resolved will not
51+
* have this context.
52+
*/
53+
export function getOsDeviceLogAttributes(client: Client): Attributes {
54+
if (attributes === undefined) {
55+
// We set attributes to an empty object to indicate that we are already fetching them
56+
attributes = {};
57+
58+
getAttributes(client)
59+
.then((attrs) => {
60+
attributes = attrs;
61+
})
62+
.catch(() => {
63+
// ignore
64+
});
65+
}
66+
67+
return attributes || {};
68+
}

src/main/sdk.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { rendererProfilingIntegration } from './integrations/renderer-profiling.
4040
import { screenshotsIntegration } from './integrations/screenshots.js';
4141
import { sentryMinidumpIntegration } from './integrations/sentry-minidump/index.js';
4242
import { configureIPC } from './ipc.js';
43+
import { getOsDeviceLogAttributes } from './log.js';
4344
import { defaultStackParser } from './stack-parse.js';
4445
import { ElectronOfflineTransportOptions, makeElectronOfflineTransport } from './transports/electron-offline-net.js';
4546
import { configureUtilityProcessIPC } from './utility-processes.js';
@@ -202,6 +203,14 @@ export function init(userOptions: ElectronMainOptions): void {
202203
client.on('beforeSendSession', addAutoIpAddressToSession);
203204
}
204205

206+
client.on('beforeCaptureLog', (log) => {
207+
log.attributes = {
208+
...log.attributes,
209+
'electron.process': 'browser',
210+
...getOsDeviceLogAttributes(client),
211+
};
212+
});
213+
205214
scope.setClient(client);
206215
client.init();
207216

test/e2e/test-apps/other/renderer-error/src/index.html

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,13 @@
1212
enableLogs: true,
1313
});
1414

15-
logger.trace('User clicked submit button', {
16-
buttonId: 'submit-form',
17-
formId: 'user-profile',
18-
timestamp: Date.now()
19-
});
20-
21-
// setTimeout(() => {
22-
// throw new Error('Some renderer error');
23-
// }, 500);
15+
setTimeout(() => {
16+
logger.trace('User clicked submit button', {
17+
buttonId: 'submit-form',
18+
formId: 'user-profile',
19+
timestamp: Date.now()
20+
});
21+
}, 500);
2422
</script>
2523
</body>
2624
</html>

test/e2e/test-apps/other/renderer-error/src/main.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@ init({
1111
});
1212

1313
app.on('ready', () => {
14-
logger.info('User profile updated', {
15-
userId: 'user_123',
16-
updatedFields: ['email', 'preferences'],
17-
});
18-
1914
const mainWindow = new BrowserWindow({
2015
show: false,
2116
webPreferences: {
@@ -25,8 +20,15 @@ app.on('ready', () => {
2520
});
2621

2722
mainWindow.loadFile(path.join(__dirname, 'index.html'));
23+
24+
setTimeout(() => {
25+
logger.info('User profile updated', {
26+
userId: 'user_123',
27+
updatedFields: ['email', 'preferences'],
28+
});
29+
}, 500);
2830
});
2931

3032
setTimeout(() => {
3133
app.quit();
32-
}, 2000);
34+
}, 4000);

test/e2e/test-apps/other/renderer-error/test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ electronTestRunner(__dirname, async (ctx) => {
2222
body: 'electron.app.ready',
2323
trace_id: UUID_MATCHER,
2424
severity_number: 9,
25-
attributes: {
25+
attributes: expect.objectContaining({
2626
'sentry.origin': { value: 'auto.electron.events', type: 'string' },
2727
'sentry.release': { value: 'javascript-logs@1.0.0', type: 'string' },
2828
'sentry.environment': { value: 'development', type: 'string' },
@@ -31,7 +31,8 @@ electronTestRunner(__dirname, async (ctx) => {
3131
'sentry.message.template': { value: 'electron.%s.%s', type: 'string' },
3232
'sentry.message.parameter.0': { value: 'app', type: 'string' },
3333
'sentry.message.parameter.1': { value: 'ready', type: 'string' },
34-
},
34+
'electron.process': { value: 'browser', type: 'string' },
35+
}),
3536
},
3637
{
3738
timestamp: expect.any(Number),
@@ -46,6 +47,9 @@ electronTestRunner(__dirname, async (ctx) => {
4647
'sentry.environment': { value: 'development', type: 'string' },
4748
'sentry.sdk.name': { value: 'sentry.javascript.electron', type: 'string' },
4849
'sentry.sdk.version': { value: SDK_VERSION, type: 'string' },
50+
'os.name': { value: expect.any(String), type: 'string' },
51+
'os.version': { value: expect.any(String), type: 'string' },
52+
'electron.process': { value: 'browser', type: 'string' },
4953
},
5054
},
5155
{
@@ -62,6 +66,9 @@ electronTestRunner(__dirname, async (ctx) => {
6266
'sentry.environment': { value: 'development', type: 'string' },
6367
'sentry.sdk.name': { value: 'sentry.javascript.electron', type: 'string' },
6468
'sentry.sdk.version': { value: SDK_VERSION, type: 'string' },
69+
'os.name': { value: expect.any(String), type: 'string' },
70+
'os.version': { value: expect.any(String), type: 'string' },
71+
'electron.process': { value: 'renderer', type: 'string' },
6572
},
6673
},
6774
]),

0 commit comments

Comments
 (0)