Skip to content

Commit a4749a4

Browse files
authored
electron, node: upload attachments after native crashes (#262)
* sdk-core: add AttachmentManager * sdk-core: add BacktraceAttachmentProvider * sdk-core: add getAttachmentProviders to BreadcrumbsStorage * fixup 7102a65 * sdk-core: add submission of attachments to existing rxids * sdk-core: add attachment database record * sdk-core: remove report on Report skipped in database * sdk-core: do not return attachment record if file does not exist * sdk-core: expose database and fileSystem to modules * sdk-core: expose timestamp in SessionFiles * node: replace createFromSession with getSessionAttachments in FileBreacrumbsStorage, expose it from package * node: add FileAttachmentsManager, add attachments from previous sessions * node: expose file system from package * node: implement postAttachment in BacktraceNodeRequestHandler * electron: implement sendAttachment in IpcReportSubmission * electron: send attachments from previous crashes to Backtrace * sdk-core: fix database unit test * sdk-core: drop count from database record * sdk-core: limit records in database per type * sdk-core: extract Attachment- and ReportBacktraceDatabaseFileRecord classes to separate files * sdk-core: add descriptions to methods in AttachmentManager * electron: join attachments into single array from previous crashes * electron: make the rxid regex more strict * sdk-core: change default maximumNumberOfAttachmentRecords to 10 * sdk-core: update param in doc in BacktraceDatabase * sdk-core: make sessionId in attachment mandatory * sdk-core: remove hash from database records --------- Co-authored-by: Sebastian Alex <[email protected]>
1 parent 5da9114 commit a4749a4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1069
-242
lines changed

packages/electron/src/common/ipc/IpcEvents.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const IpcEvents = {
77
addSummedMetric: `${prefix}_addSummedMetric`,
88
sendMetrics: `${prefix}_sendMetrics`,
99
sendReport: `${prefix}_sendReport`,
10+
sendAttachment: `${prefix}_sendAttachment`,
1011
post: `${prefix}_post`,
1112
addBreadcrumb: `${prefix}_addBreadcrumb`,
1213
sync: `${prefix}_sync`,

packages/electron/src/main/modules/BacktraceMainElectronModule.ts

+79-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import { FileAttachmentsManager, FileBreadcrumbsStorage, NodeFileSystem } from '@backtrace/node';
12
import {
23
BacktraceData,
34
BacktraceModule,
45
BacktraceModuleBindData,
56
RawBreadcrumb,
7+
SessionFiles,
68
SubmissionUrlInformation,
79
SummedEvent,
810
} from '@backtrace/sdk-core';
11+
import type { BacktraceDatabase } from '@backtrace/sdk-core/lib/modules/database/BacktraceDatabase';
912
import { app, crashReporter } from 'electron';
1013
import { IpcAttachmentReference } from '../../common/ipc/IpcAttachmentReference';
1114
import { IpcEvents } from '../../common/ipc/IpcEvents';
@@ -53,6 +56,15 @@ export class BacktraceMainElectronModule implements BacktraceModule {
5356
return await reportSubmission.send(data, [...attachments, ...client.attachments]);
5457
});
5558

59+
rpc.on(IpcEvents.sendAttachment, async (event, rxid: string, attachmentRef: IpcAttachmentReference) => {
60+
const attachment = new IpcAttachment(
61+
attachmentRef.name,
62+
attachmentRef.id,
63+
new WindowIpcTransport(event.sender),
64+
);
65+
return await reportSubmission.sendAttachment(rxid, attachment);
66+
});
67+
5668
rpc.on(IpcEvents.sendMetrics, async () => client.metrics?.send());
5769

5870
rpc.on(IpcEvents.ping, () => Promise.resolve('pong'));
@@ -90,7 +102,8 @@ export class BacktraceMainElectronModule implements BacktraceModule {
90102
return;
91103
}
92104

93-
const { options, attributeManager } = this._bindData;
105+
const { options, attributeManager, sessionFiles, fileSystem, database } = this._bindData;
106+
94107
if (options.database?.captureNativeCrashes) {
95108
if (options.database.path) {
96109
app.setPath('crashDumps', options.database.path);
@@ -111,6 +124,13 @@ export class BacktraceMainElectronModule implements BacktraceModule {
111124
crashReporter.addExtraParameter(key, dict[key]);
112125
}
113126
});
127+
128+
if (sessionFiles && database && fileSystem) {
129+
const lockId = sessionFiles.lockPreviousSessions();
130+
this.sendPreviousCrashAttachments(database, sessionFiles, fileSystem as NodeFileSystem).finally(
131+
() => lockId && sessionFiles.unlockPreviousSessions(lockId),
132+
);
133+
}
114134
}
115135
}
116136

@@ -121,6 +141,64 @@ export class BacktraceMainElectronModule implements BacktraceModule {
121141
'electron.process': 'renderer',
122142
};
123143
}
144+
145+
private async sendPreviousCrashAttachments(
146+
database: BacktraceDatabase,
147+
session: SessionFiles,
148+
fileSystem: NodeFileSystem,
149+
) {
150+
// Sort crashes and sessions by timestamp descending
151+
const crashes = crashReporter.getUploadedReports().sort((a, b) => b.date.getTime() - a.date.getTime());
152+
const previousSessions = session.getPreviousSessions().sort((a, b) => b.timestamp - a.timestamp);
153+
154+
for (const crash of crashes) {
155+
const rxid = this.getCrashRxid(crash.id);
156+
if (!rxid) {
157+
continue;
158+
}
159+
160+
try {
161+
// Get first session that happened before the crash
162+
const session = previousSessions.find((p) => p.timestamp < crash.date.getTime());
163+
// If there is no such session, there won't be any other sessions
164+
if (!session) {
165+
break;
166+
}
167+
168+
const crashLock = session.getFileName(`electron-crash-lock-${rxid}`);
169+
// If crash lock exists, do not attempt to add attachments twice
170+
if (await fileSystem.exists(crashLock)) {
171+
continue;
172+
}
173+
174+
const fileAttachmentsManager = FileAttachmentsManager.createFromSession(session, fileSystem);
175+
const sessionAttachments = [
176+
...FileBreadcrumbsStorage.getSessionAttachments(session),
177+
...(await fileAttachmentsManager.get()),
178+
];
179+
180+
for (const attachment of sessionAttachments) {
181+
database.addAttachment(rxid, attachment, session.sessionId);
182+
}
183+
184+
// Write an empty crash lock, so we know that this crash is already taken care of
185+
await fileSystem.writeFile(crashLock, '');
186+
} catch {
187+
// Do nothing, skip the report
188+
}
189+
}
190+
191+
await database.send();
192+
}
193+
194+
private getCrashRxid(crashId: string): string | undefined {
195+
try {
196+
return JSON.parse(crashId)._rxid;
197+
} catch {
198+
const rxidRegex = /"_rxid":\s*"([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})"/i;
199+
return crashId.match(rxidRegex)?.[1];
200+
}
201+
}
124202
}
125203

126204
function toStringDictionary(record: Record<string, unknown>): Record<string, string> {

packages/electron/src/renderer/modules/IpcReportSubmission.ts

+38-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
BacktraceAttachment,
3+
BacktraceAttachmentResponse,
34
BacktraceData,
45
BacktraceReportSubmission,
56
BacktraceReportSubmissionResult,
@@ -32,22 +33,8 @@ export class IpcReportSubmission implements BacktraceReportSubmission {
3233
}
3334

3435
for (const attachment of attachments) {
35-
const id = IdGenerator.uuid() + '_' + attachment.name;
36-
const content = attachment.get();
37-
if (content instanceof Blob) {
38-
const stream = new WritableIpcStream(id, this._ipcTransport);
39-
content.stream().pipeTo(stream);
40-
} else if (content instanceof ReadableStream) {
41-
const stream = new WritableIpcStream(id, this._ipcTransport);
42-
content.pipeTo(stream);
43-
} else if (content != undefined) {
44-
const stream = new WritableIpcStream(id, this._ipcTransport);
45-
const writer = stream.getWriter();
46-
writer
47-
.write(typeof content === 'string' ? content : JSON.stringify(content, jsonEscaper()))
48-
.then(() => writer.releaseLock())
49-
.then(() => stream.close());
50-
} else {
36+
const id = this.pipeAttachment(attachment);
37+
if (!id) {
5138
continue;
5239
}
5340

@@ -59,4 +46,39 @@ export class IpcReportSubmission implements BacktraceReportSubmission {
5946

6047
return this._ipcRpc.invoke(IpcEvents.sendReport, data, references);
6148
}
49+
50+
public async sendAttachment(
51+
rxid: string,
52+
attachment: BacktraceAttachment,
53+
): Promise<BacktraceReportSubmissionResult<BacktraceAttachmentResponse>> {
54+
const id = this.pipeAttachment(attachment);
55+
if (!id) {
56+
return BacktraceReportSubmissionResult.ReportSkipped();
57+
}
58+
59+
return this._ipcRpc.invoke(IpcEvents.sendAttachment, rxid, { id, name: attachment.name });
60+
}
61+
62+
private pipeAttachment(attachment: BacktraceAttachment) {
63+
const id = IdGenerator.uuid() + '_' + attachment.name;
64+
const content = attachment.get();
65+
if (content instanceof Blob) {
66+
const stream = new WritableIpcStream(id, this._ipcTransport);
67+
content.stream().pipeTo(stream);
68+
} else if (content instanceof ReadableStream) {
69+
const stream = new WritableIpcStream(id, this._ipcTransport);
70+
content.pipeTo(stream);
71+
} else if (content != undefined) {
72+
const stream = new WritableIpcStream(id, this._ipcTransport);
73+
const writer = stream.getWriter();
74+
writer
75+
.write(typeof content === 'string' ? content : JSON.stringify(content, jsonEscaper()))
76+
.then(() => writer.releaseLock())
77+
.then(() => stream.close());
78+
} else {
79+
return undefined;
80+
}
81+
82+
return id;
83+
}
6284
}

packages/node/src/BacktraceClient.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import path from 'path';
1111
import { BacktraceConfiguration, BacktraceSetupConfiguration } from './BacktraceConfiguration';
1212
import { BacktraceNodeRequestHandler } from './BacktraceNodeRequestHandler';
1313
import { AGENT } from './agentDefinition';
14+
import { FileAttachmentsManager } from './attachment/FileAttachmentsManager';
1415
import { transformAttachment } from './attachment/transformAttachments';
1516
import { FileBreadcrumbsStorage } from './breadcrumbs/FileBreadcrumbsStorage';
1617
import { BacktraceClientBuilder } from './builder/BacktraceClientBuilder';
@@ -54,6 +55,7 @@ export class BacktraceClient extends BacktraceCoreClient<BacktraceConfiguration>
5455

5556
if (this.sessionFiles && clientSetup.options.database?.captureNativeCrashes) {
5657
this.addModule(FileAttributeManager, FileAttributeManager.create(fileSystem));
58+
this.addModule(FileAttachmentsManager, FileAttachmentsManager.create(fileSystem));
5759
}
5860
}
5961

@@ -299,14 +301,14 @@ export class BacktraceClient extends BacktraceCoreClient<BacktraceConfiguration>
299301
for (const [recordPath, report, session] of reports) {
300302
try {
301303
if (session) {
302-
const breadcrumbsStorage = FileBreadcrumbsStorage.createFromSession(session, this.nodeFileSystem);
303-
if (breadcrumbsStorage) {
304-
report.attachments.push(...breadcrumbsStorage.getAttachments());
305-
}
304+
report.attachments.push(...FileBreadcrumbsStorage.getSessionAttachments(session));
306305

307306
const fileAttributes = FileAttributeManager.createFromSession(session, this.nodeFileSystem);
308307
Object.assign(report.attributes, await fileAttributes.get());
309308

309+
const fileAttachments = FileAttachmentsManager.createFromSession(session, this.nodeFileSystem);
310+
report.attachments.push(...(await fileAttachments.get()));
311+
310312
report.attributes['application.session'] = session.sessionId;
311313
} else {
312314
report.attributes['application.session'] = null;

0 commit comments

Comments
 (0)