Skip to content

Commit

Permalink
feat: Offline Support (#392)
Browse files Browse the repository at this point in the history
  • Loading branch information
timfish authored Dec 15, 2021
1 parent 05638da commit 12058e4
Show file tree
Hide file tree
Showing 33 changed files with 752 additions and 253 deletions.
27 changes: 6 additions & 21 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: '14'
- uses: actions/cache@v2
with:
path: |
.cache/**
**/node_modules
key: ${{ runner.os }}
cache: 'yarn'
- name: Install
run: yarn install
- name: Build
Expand All @@ -48,12 +43,7 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: '14'
- uses: actions/cache@v2
with:
path: |
.cache/**
**/node_modules
key: ${{ runner.os }}
cache: 'yarn'
- run: yarn install
- name: Run Linter
run: yarn lint
Expand All @@ -72,12 +62,7 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: '14'
- uses: actions/cache@v2
with:
path: |
.cache/**
**/node_modules
key: ${{ runner.os }}
cache: 'yarn'
- run: yarn install
- name: Run Unit Tests
run: yarn test
Expand All @@ -98,12 +83,12 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: '14'
cache: 'yarn'
- uses: actions/cache@v2
with:
path: |
.cache/**
**/node_modules
key: ${{ runner.os }}
**/.cache/**/*.zip
key: ${{ runner.os }}-${{ matrix.electron }}
- run: yarn install
- name: Run E2E Tests
run: yarn e2e
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## Unreleased

## 3.0.0-beta.4

- feat: Adds `ElectronOfflineNetTransport` and makes it the default transport
- feat: Adds `AdditionalContext` integration that includes additional device and enables it by default (#390)
- feat: Renames `ElectronEvents ` to `ElectronBreadcrumbs` and allows more complex configuration
- fix: Fixes bundling of preload code (#396)
- feat: Adds breadcrumbs and tracing for the Electron `net` module (#395)
- feat: Breadcrumbs and events for child processes (#398)
- feat: Capture minidumps for GPU crashes with `SentryMinidump` integration (#398)

## 3.0.0-beta.3

- fix: Enable CORS for custom protocol
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"@types/koa": "^2.0.52",
"@types/koa-bodyparser": "^4.3.0",
"@types/mocha": "^9.0.0",
"@types/tmp": "^0.2.2",
"busboy": "^0.3.1",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
Expand All @@ -95,6 +96,7 @@
"npm-run-all": "^4.1.5",
"prettier": "^2.4.1",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
"ts-node": "^10.4.0",
"typescript": "^4.4.4",
"xvfb-maybe": "^0.2.1",
Expand Down
118 changes: 14 additions & 104 deletions src/main/fs.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,19 @@
import { mkdir, mkdirSync, readdir, readFile, rename, stat, Stats, statSync, unlink } from 'fs';
import { dirname, resolve } from 'path';
import { app } from 'electron';
import { mkdir, mkdirSync, readdir, readFile, rename, stat, statSync, unlink, writeFile } from 'fs';
import { dirname, join, resolve } from 'path';
import { promisify } from 'util';

/**
* Asynchronously reads given files content.
*
* @param path A relative or absolute path to the file
* @returns A Promise that resolves when the file has been read.
*/
export async function readFileAsync(
path: string,
options?: { encoding?: string; flag?: string },
): Promise<string | Buffer> {
// We cannot use util.promisify here because that was only introduced in Node
// 8 and we need to support older Node versions.
return new Promise<string | Buffer>((res, reject) => {
readFile(path, options as any, (err: any, data: any) => {
if (err) {
reject(err);
} else {
res(data);
}
});
});
}
export const sentryCachePath = join(app.getPath('userData'), 'sentry');

/**
* Asynchronously creates the given directory.
*
* @param path A relative or absolute path to the directory.
* @param mode The permission mode.
* @returns A Promise that resolves when the path has been created.
*/
async function mkdirAsync(path: string, mode: number): Promise<void> {
// We cannot use util.promisify here because that was only introduced in Node
// 8 and we need to support older Node versions.
return new Promise<void>((res, reject) => {
mkdir(path, mode, (err) => {
if (err) {
reject(err);
} else {
res();
}
});
});
}
export const writeFileAsync = promisify(writeFile);
export const readFileAsync = promisify(readFile);
export const mkdirAsync = promisify(mkdir);
export const statAsync = promisify(stat);
export const unlinkAsync = promisify(unlink);
export const readDirAsync = promisify(readdir);
export const renameAsync = promisify(rename);

// mkdir/mkdirSync with recursive was only added in Node 10+

/**
* Recursively creates the given path.
Expand Down Expand Up @@ -100,63 +70,3 @@ export function mkdirpSync(path: string): void {
}
}
}

/**
* Read stats async
*/
export function statAsync(path: string): Promise<Stats> {
return new Promise<Stats>((res, reject) => {
stat(path, (err, stats) => {
if (err) {
reject(err);
return;
}
res(stats);
});
});
}

/**
* unlink async
*/
export function unlinkAsync(path: string): Promise<void> {
return new Promise<void>((res, reject) => {
unlink(path, (err) => {
if (err) {
reject(err);
return;
}
res();
});
});
}

/**
* readdir async
*/
export function readDirAsync(path: string): Promise<string[]> {
return new Promise<string[]>((res, reject) => {
readdir(path, (err, files) => {
if (err) {
reject(err);
return;
}
res(files);
});
});
}

/**
* rename async
*/
export function renameAsync(oldPath: string, newPath: string): Promise<void> {
return new Promise<void>((res, reject) => {
rename(oldPath, newPath, (err) => {
if (err) {
reject(err);
return;
}
res();
});
});
}
1 change: 1 addition & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export {
export { NodeOptions, NodeBackend, NodeClient, lastEventId } from '@sentry/node';

export { ElectronNetTransport } from './transports/electron-net';
export { ElectronOfflineNetTransport } from './transports/electron-offline-net';
export const Integrations = { ...ElectronMainIntegrations, ...NodeIntegrations };

export { init, ElectronMainOptions, defaultIntegrations } from './sdk';
Expand Down
86 changes: 5 additions & 81 deletions src/main/integrations/sentry-minidump/base-uploader.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import { API } from '@sentry/core';
import { NodeOptions } from '@sentry/node';
import { Event, Status, Transport } from '@sentry/types';
import { Event, Transport } from '@sentry/types';
import { isThenable, logger, SentryError, timestampWithMs } from '@sentry/utils';
import { basename, join } from 'path';
import { basename } from 'path';

import { mkdirp, readFileAsync, renameAsync, statAsync, unlinkAsync } from '../../fs';
import { Store } from '../../store';
import { readFileAsync, statAsync, unlinkAsync } from '../../fs';
import { ElectronNetTransport, SentryElectronRequest } from '../../transports/electron-net';

/** Maximum number of days to keep a minidump before deleting it. */
const MAX_AGE = 30;

/** Maximum number of requests that we store/queue if something goes wrong. */
const MAX_REQUESTS_COUNT = 10;

/**
* Payload for a minidump request comprising a persistent file system path and
* event metadata.
Expand All @@ -32,31 +28,20 @@ export abstract class BaseUploader {
/** List of minidumps that have been found already. */
private readonly _knownPaths: string[];

/**
* Store to persist queued Minidumps beyond application crashes or lost
* internet connection.
*/
private readonly _queue: Store<MinidumpRequest[]>;

/** API object */
private readonly _api: API;

/**
* Creates a new uploader instance.
*/
public constructor(
private readonly _options: NodeOptions,
private readonly _cacheDirectory: string,
private readonly _transport: Transport,
) {
public constructor(private readonly _options: NodeOptions, private readonly _transport: Transport) {
this._knownPaths = [];

if (!_options.dsn) {
throw new SentryError('Attempted to enable Electron native crash reporter but no DSN was supplied');
}

this._api = new API(_options.dsn);
this._queue = new Store(this._cacheDirectory, 'queue', []);
}

/**
Expand Down Expand Up @@ -90,14 +75,10 @@ export abstract class BaseUploader {

const transport = this._transport as ElectronNetTransport;
try {
let response;

if (request.event && !transport.isRateLimited('event')) {
logger.log('Sending minidump', request.path);

const requestForTransport = await this._toMinidumpRequest(transport, request.event, request.path);
response = await transport.sendRequest(requestForTransport);
logger.log('Minidump sent');
await transport.sendRequest(requestForTransport);
}

// We either succeeded, something went wrong or the send was aborted. Either way, we can remove the minidump file.
Expand All @@ -109,22 +90,10 @@ export abstract class BaseUploader {
}

// Forget this minidump in all caches
this._queue.update((queued) => queued.filter((stored) => stored !== request));
this._knownPaths.splice(this._knownPaths.indexOf(request.path), 1);

// If we were successful, we can try to flush the remaining queue
if (response && response.status === Status.Success) {
await this.flushQueue();
}
} catch (err) {
// TODO: Test this
logger.warn('Failed to upload minidump', err);

// User's internet connection was down so we queue it as well
const error = err ? (err as { code: string }) : { code: '' };
if (error.code === 'ENOTFOUND') {
await this._queueMinidump(request);
}
}
}

Expand Down Expand Up @@ -164,11 +133,6 @@ export abstract class BaseUploader {
});
}

/** Flushes locally cached minidumps from the queue. */
public async flushQueue(): Promise<void> {
await Promise.all(this._queue.get().map(async (request) => this.uploadMinidump(request)));
}

/**
* Helper to filter an array with asynchronous callbacks.
*
Expand All @@ -186,46 +150,6 @@ export abstract class BaseUploader {
return array.filter((_, index) => verdicts[index]);
}

/**
* Enqueues a minidump with event information for later upload.
* @param request The request containing a minidump and event info.
*/
private async _queueMinidump(request: MinidumpRequest): Promise<void> {
const filename = basename(request.path);

// Only enqueue if this minidump hasn't been enqueued before. Compare the
// filename instead of the full path, because we will move the file to a
// temporary location later on.
if (this._queue.get().some((req) => basename(req.path) === filename)) {
return;
}

// Move the minidump file to a separate cache directory and enqueue it. Even
// if the Electron CrashReporter's cache directory gets wiped or changes,
// this will allow us to retry uploading the file later.
const queuePath = join(this._cacheDirectory, filename);
await mkdirp(this._cacheDirectory);
await renameAsync(request.path, queuePath);

// Remove stale minidumps in case we go over limit. Note that we have to
// re-fetch the queue as it might have changed in the meanwhile. It is
// important to store the queue value again immediately to avoid phantom
// reads.
const requests = [...this._queue.get(), { ...request, path: queuePath }];
const stale = requests.splice(-MAX_REQUESTS_COUNT);
this._queue.set(requests);

await Promise.all(
stale.map(async (req) => {
try {
await unlinkAsync(req.path);
} catch (e) {
logger.warn('Could not delete', req.path);
}
}),
);
}

/**
* Create minidump request to dispatch to the transport
*/
Expand Down
4 changes: 2 additions & 2 deletions src/main/integrations/sentry-minidump/breakpad-uploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { BaseUploader } from './base-uploader';
export class BreakpadUploader extends BaseUploader {
private readonly _crashesDirectory: string = getCrashesDirectory();

public constructor(options: NodeOptions, cacheDirectory: string, transport: Transport) {
super(options, cacheDirectory, transport);
public constructor(options: NodeOptions, transport: Transport) {
super(options, transport);
}

/** @inheritdoc */
Expand Down
Loading

0 comments on commit 12058e4

Please sign in to comment.