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
11 changes: 11 additions & 0 deletions src/common/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ export function getScopeData(): ScopeData {
return globalScope;
}

/**
* Calls back on any change to the global, isolation or current scope
* without building the merged scope. Use this when the callback reads
* the scope lazily, so bursts of changes cost nothing until it does.
*/
export function onScopeChange(callback: () => void): void {
getGlobalScope().addScopeListener(callback);
getIsolationScope().addScopeListener(callback);
getCurrentScope().addScopeListener(callback);
}

/** Hooks both current and isolation scope changes and passes merged scope on changes */
export function addScopeListener(callback: (merged: ScopeData, changed: Scope) => void): void {
getIsolationScope().addScopeListener((isolation) => {
Expand Down
6 changes: 6 additions & 0 deletions src/main/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ export function getDefaultEnvironment(): string {
return app.isPackaged ? 'production' : 'development';
}

/**
* How long scope saves may reuse the event defaults. Building them runs
* every event processor and some are slow, such as `app.getAppMetrics()`.
*/
export const EVENT_DEFAULTS_TTL_MS = 5_000;

/**
* Gets the default event data by calling all event processors.
*/
Expand Down
65 changes: 31 additions & 34 deletions src/main/integrations/electron-minidump.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import type { Event, ScopeData } from '@sentry/core';
import { applyScopeDataToEvent, debug, defineIntegration, makeDsn, uuid4 } from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { app, crashReporter } from 'electron';
import { addScopeListener, getScopeData } from '../../common/scope.js';
import { getEventDefaults, getSdkInfo } from '../context.js';
import { getScopeData, onScopeChange } from '../../common/scope.js';
import { EVENT_DEFAULTS_TTL_MS, getEventDefaults, getSdkInfo } from '../context.js';
import { CRASH_REASONS } from '../electron-normalize.js';
import { mergeEvents } from '../merge.js';
import { normalizePaths } from '../normalize.js';
import { createScopeSaver } from '../scope-updates.js';
import { checkPreviousSession, sessionCrashed, unreportedDuringLastSession } from '../sessions.js';

/** Is object defined and has keys */
Expand Down Expand Up @@ -93,14 +94,12 @@ export function minidumpUrlFromDsn(dsn: string): string | undefined {
* Sends minidumps via the Electron built-in uploader.
*/
export const electronMinidumpIntegration = defineIntegration(() => {
/** Counter used to ensure no race condition when updating extra params */
let updateEpoch = 0;
let lastParamCount = 0;

async function getNativeUploaderEvent(client: NodeClient, scope: ScopeData): Promise<Event> {
function getNativeUploaderEvent(client: NodeClient, scope: ScopeData, defaults: Event): Event {
const { userInfo: inferIpAddress } = client.getDataCollectionOptions();

const event = mergeEvents(await getEventDefaults(client), {
const event = mergeEvents(defaults, {
sdk: getSdkInfo(inferIpAddress),
event_id: uuid4(),
level: 'fatal',
Expand All @@ -116,35 +115,26 @@ export const electronMinidumpIntegration = defineIntegration(() => {
return normalizePaths(event, app.getAppPath());
}

function updateExtraParams(client: NodeClient, scope: ScopeData): void {
updateEpoch += 1;
const currentEpoch = updateEpoch;
function updateExtraParams(client: NodeClient, scope: ScopeData, defaults: Event): void {
const event = getNativeUploaderEvent(client, scope, defaults);

getNativeUploaderEvent(client, scope)
.then((event) => {
if (currentEpoch !== updateEpoch) {
return;
}

// Update the extra parameters in the main process
const mainParams = getNativeUploaderExtraParams(event);
const count = Object.keys(mainParams).length;
// Update the extra parameters in the main process
const mainParams = getNativeUploaderExtraParams(event);
const count = Object.keys(mainParams).length;

// Remove any extra parameters that extend beyond the current count
if (lastParamCount > count) {
for (let i = count + 1; i <= lastParamCount; i++) {
crashReporter.removeExtraParameter(`sentry__${i}`);
}
}
// Remove any extra parameters that extend beyond the current count
if (lastParamCount > count) {
for (let i = count + 1; i <= lastParamCount; i++) {
crashReporter.removeExtraParameter(`sentry__${i}`);
}
}

for (const [key, value] of Object.entries(mainParams)) {
crashReporter.addExtraParameter(key, value);
}
for (const [key, value] of Object.entries(mainParams)) {
crashReporter.addExtraParameter(key, value);
}

// Track how many parameters we added so we can remove extras next time
lastParamCount = count;
})
.catch((error) => debug.error(error));
// Track how many parameters we added so we can remove extras next time
lastParamCount = count;
}

function startCrashReporter(options: NodeOptions): void {
Expand Down Expand Up @@ -172,9 +162,16 @@ export const electronMinidumpIntegration = defineIntegration(() => {
}

function setupScopeListener(client: NodeClient): void {
addScopeListener((scope) => {
updateExtraParams(client, scope);
});
const saver = createScopeSaver<ScopeData, Event>(
getScopeData,
() => getEventDefaults(client),
(scope, defaults) => updateExtraParams(client, scope, defaults),
EVENT_DEFAULTS_TTL_MS,
);

onScopeChange(saver.save);
// Screen and GPU context only exist once the app is ready
void app.whenReady().then(saver.refresh);
}

return {
Expand Down
32 changes: 14 additions & 18 deletions src/main/integrations/sentry-minidump/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { applyScopeDataToEvent, captureEvent, createStackParser, debug, defineIn
import type { NodeClient } from '@sentry/node';
import { chromeStackLineParser } from '@sentry/browser';
import { app, crashReporter } from 'electron';
import { addScopeListener, getScopeData } from '../../../common/scope.js';
import { getEventDefaults } from '../../context.js';
import { getScopeData, onScopeChange } from '../../../common/scope.js';
import { EVENT_DEFAULTS_TTL_MS, getEventDefaults } from '../../context.js';
import { EXIT_REASONS, getSentryCachePath } from '../../electron-normalize.js';
import { getRendererProperties, trackRendererProperties } from '../../renderers.js';
import { createScopeSaver } from '../../scope-updates.js';
import type { ElectronMainOptions } from '../../sdk.js';
import { previousSessionWasAbnormal, restorePreviousSession, setPreviousSessionAsCurrent } from '../../sessions.js';
import { BufferedWriteStore } from '../../store.js';
Expand Down Expand Up @@ -58,22 +59,17 @@ export const sentryMinidumpIntegration = defineIntegration((options: Options = {
}

function setupScopeListener(client: NodeClient): void {
function scopeChanged(scope: ScopeData): void {
// Since the initial scope read is async, we need to ensure that any writes do not beat that
// https://github.com/getsentry/sentry-electron/issues/585
setImmediate(async () =>
scopeStore?.set({
scope,
event: await getEventDefaults(client),
}),
);
}

addScopeListener((scope) => {
scopeChanged(scope);
});

scopeChanged(getScopeData());
const saver = createScopeSaver<ScopeData, Event>(
getScopeData,
() => getEventDefaults(client),
(scope, event) => scopeStore?.set({ scope, event }),
EVENT_DEFAULTS_TTL_MS,
);

onScopeChange(saver.save);
// Screen and GPU context only exist once the app is ready
void app.whenReady().then(saver.refresh);
saver.save();
}

async function sendNativeCrashes(
Expand Down
80 changes: 80 additions & 0 deletions src/main/scope-updates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { debug } from '@sentry/core';

export interface ScopeSaver {
/** Saves the current scope on the next event loop turn */
save: () => void;
/** Drops the cached defaults and saves the current scope */
refresh: () => void;
}

/**
* Creates a saver that writes the current scope together with event
* defaults that are at most `ttlMs` old.
*
* Calls in one event loop turn are merged into one save on the next
* turn, and the scope is read once when that save runs. The defaults
* are rebuilt only when a save finds them expired, or after `refresh`.
*/
export function createScopeSaver<TScope, TDefaults>(
getScope: () => TScope,
getDefaults: () => Promise<TDefaults>,
save: (scope: TScope, defaults: TDefaults) => void | Promise<void>,
ttlMs: number,
): ScopeSaver {
let scheduled = false;
let runs = 0;
let defaults: Promise<TDefaults> | undefined;
let expires = 0;

function schedule(): void {
if (scheduled) {
return;
}

scheduled = true;

// Deferring to the next turn ensures that the first save does not
// beat the initial async read of the store
// https://github.com/getsentry/sentry-electron/issues/585
setImmediate(() => {
run().catch((error) => debug.error(error));
});
}

async function run(): Promise<void> {
scheduled = false;
const thisRun = ++runs;
const scope = getScope();
const now = Date.now();

if (!defaults || now >= expires) {
const current = getDefaults();
defaults = current;
expires = now + ttlMs;

current.catch(() => {
if (defaults === current) {
defaults = undefined;
}
});
}

const resolved = await defaults;

// A newer run holds a newer scope, so a slow older one must not
// write over it
if (thisRun !== runs) {
return;
}

await save(scope, resolved);
}

return {
save: schedule,
refresh: () => {
defaults = undefined;
schedule();
},
};
}
Loading
Loading