Skip to content

Commit

Permalink
switch to sentry-electron (Jigsaw-Code#232)
Browse files Browse the repository at this point in the history
  • Loading branch information
trevj authored Aug 17, 2018
1 parent 50364b1 commit 69cf89c
Show file tree
Hide file tree
Showing 18 changed files with 403 additions and 235 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
- yarn do shadowbox/server/build
- yarn do shadowbox/test
- yarn do server_manager/electron_app/build
- yarn do server_manager/electron_app/test
- yarn do server_manager/web_app/build
- yarn do server_manager/web_app/test

Expand Down
3 changes: 2 additions & 1 deletion jasmine.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"spec_dir": ".",
"spec_files": [
"build/**/*.spec.js"
"build/server_manager/electron_app/js/**/*.spec.js",
"build/server_manager/web_app/**/*.spec.js"
],
"helpers": ["src/base64mocks.js"],
"stopSpecOnExpectationFailure": false,
Expand Down
21 changes: 10 additions & 11 deletions src/server_manager/cloud/digitalocean_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import * as events from 'events';

import * as errors from '../infrastructure/errors';
import {SentryErrorReporter} from '../web_app/error_reporter';

export interface DigitalOceanDropletSpecification {
installCommand: string;
Expand Down Expand Up @@ -102,7 +101,7 @@ class RestApiSession implements DigitalOceanSession {
constructor(public accessToken: string) {}

public getAccount(): Promise<Account> {
SentryErrorReporter.logInfo('Requesting account');
console.info('Requesting account');
return this.request<{account: Account}>('GET', 'account/').then((response) => {
return response.account;
});
Expand All @@ -129,7 +128,7 @@ class RestApiSession implements DigitalOceanSession {
return new Promise((fulfill, reject) => {
const makeRequestRecursive = () => {
++requestCount;
SentryErrorReporter.logInfo(`Requesting droplet creation ${requestCount}/${MAX_REQUESTS}`);
console.info(`Requesting droplet creation ${requestCount}/${MAX_REQUESTS}`);
this.request<{droplet: DropletInfo}>('POST', 'droplets', {
name: dropletName,
region,
Expand Down Expand Up @@ -158,20 +157,20 @@ class RestApiSession implements DigitalOceanSession {
}

public deleteDroplet(dropletId: number): Promise<void> {
SentryErrorReporter.logInfo('Requesting droplet deletion');
console.info('Requesting droplet deletion');
return this.request<void>('DELETE', 'droplets/' + dropletId);
}

public getRegionInfo(): Promise<RegionInfo[]> {
SentryErrorReporter.logInfo('Requesting region info');
console.info('Requesting region info');
return this.request<{regions: RegionInfo[]}>('GET', 'regions').then((response) => {
return response.regions;
});
}

// Registers a SSH key with DigitalOcean.
private registerKey_(keyName: string, publicKeyForSSH: string): Promise<number> {
SentryErrorReporter.logInfo('Requesting key registration');
console.info('Requesting key registration');
return this
.request<{ssh_key: {id: number}}>(
'POST', 'account/keys', {name: keyName, public_key: publicKeyForSSH})
Expand All @@ -181,7 +180,7 @@ class RestApiSession implements DigitalOceanSession {
}

public getDroplet(dropletId: number): Promise<DropletInfo> {
SentryErrorReporter.logInfo('Requesting droplet');
console.info('Requesting droplet');
return this.request<{droplet: DropletInfo}>('GET', 'droplets/' + dropletId).then((response) => {
return response.droplet;
});
Expand All @@ -194,15 +193,15 @@ class RestApiSession implements DigitalOceanSession {
}

public getDropletsByTag(tag: string): Promise<DropletInfo[]> {
SentryErrorReporter.logInfo('Requesting droplet by tag');
console.info('Requesting droplet by tag');
return this.request<{droplets: DropletInfo[]}>('GET', `droplets/?tag_name=${encodeURI(tag)}`)
.then((response) => {
return response.droplets;
});
}

public getDroplets(): Promise<DropletInfo[]> {
SentryErrorReporter.logInfo('Requesting droplets');
console.info('Requesting droplets');
return this.request<{droplets: DropletInfo[]}>('GET', 'droplets/').then((response) => {
return response.droplets;
});
Expand All @@ -226,7 +225,7 @@ class RestApiSession implements DigitalOceanSession {
} else {
// this.response is a JSON object, whose message is an error string.
const responseJson = JSON.parse(xhr.response);
SentryErrorReporter.logError(`DigitalOcean request failed with status ${xhr.status}`);
console.error(`DigitalOcean request failed with status ${xhr.status}`);
reject(new Error(
`XHR ${responseJson.id} failed with ${xhr.status}: ${responseJson.message}`));
}
Expand All @@ -240,7 +239,7 @@ class RestApiSession implements DigitalOceanSession {
// DigitalOcean (this isn't so bad because application-level
// errors, e.g. bad request parameters and even 404s, do *not* raise
// an onerror event).
SentryErrorReporter.logError('Failed to perform DigitalOcean request');
console.error('Failed to perform DigitalOcean request');
reject(new XhrError());
};
xhr.send(data ? JSON.stringify(data) : undefined);
Expand Down
53 changes: 40 additions & 13 deletions src/server_manager/electron_app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import * as sentry from '@sentry/electron';
import * as electron from 'electron';
import {autoUpdater} from 'electron-updater';
import * as fs from 'fs';
import * as path from 'path';
import * as url from 'url';
import {URL, URLSearchParams} from 'url';

import {LoadingWindow} from './loading_window';
import * as menu from './menu';
import {redactManagerUrl} from './util';

const app = electron.app;
const ipcMain = electron.ipcMain;
Expand All @@ -30,14 +31,43 @@ const debugMode = process.env.OUTLINE_DEBUG === 'true';
const IMAGES_BASENAME =
`${path.join(__dirname.replace('app.asar', 'app.asar.unpacked'), 'server_manager', 'web_app')}`;

const sentryDsn =
process.env.SENTRY_DSN || 'https://533e56d1b2d64314bd6092a574e6d0f1@sentry.io/215496';

sentry.init({
dsn: sentryDsn,
// Sentry provides a sensible default but we would prefer without the leading "outline-manager@".
release: electron.app.getVersion(),
maxBreadcrumbs: 100,
shouldAddBreadcrumb: (breadcrumb) => {
// Don't submit breadcrumbs for console.debug.
if (breadcrumb.category === 'console') {
if (breadcrumb.level === sentry.Severity.Debug) {
return false;
}
}
return true;
},
beforeBreadcrumb: (breadcrumb) => {
// Redact PII from XHR requests.
if (breadcrumb.category === 'fetch' && breadcrumb.data && breadcrumb.data.url) {
try {
breadcrumb.data.url = `(redacted)/${redactManagerUrl(breadcrumb.data.url)}`;
} catch (e) {
// NOTE: cannot log this failure to console if console breadcrumbs are enabled
breadcrumb.data.url = `(error redacting)`;
}
}
return breadcrumb;
}
});
// To clearly identify app restarts in Sentry.
console.info(`Outline Manager is starting`);

interface IpcEvent {
returnValue: {};
}

function startsWith(larger: string, prefix: string) {
return larger.substr(0, prefix.length) === prefix;
}

function createMainWindow() {
const win = new electron.BrowserWindow({
width: 600,
Expand Down Expand Up @@ -82,7 +112,7 @@ function createMainWindow() {
}

function getWebAppUrl() {
const queryParams = new url.URLSearchParams();
const queryParams = new URLSearchParams();
queryParams.set('version', electron.app.getVersion());

// Set queryParams from environment variables.
Expand All @@ -94,17 +124,14 @@ function getWebAppUrl() {
queryParams.set('metricsUrl', process.env.SB_METRICS_URL);
console.log(`Will use metrics url ${process.env.SB_METRICS_URL}`);
}
if (process.env.SENTRY_DSN) {
queryParams.set('sentryDsn', process.env.SENTRY_DSN);
console.log(`Will use sentryDsn url ${process.env.SENTRY_DSN}`);
}
queryParams.set('sentryDsn', sentryDsn);
if (debugMode) {
queryParams.set('outlineDebugMode', 'true');
console.log(`Enabling Outline debug mode`);
}

// Append arguments to URL if any.
const webAppUrl = new url.URL('outline://web_app/index.html');
const webAppUrl = new URL('outline://web_app/index.html');
webAppUrl.search = queryParams.toString();
const webAppUrlString = webAppUrl.toString();
console.log('Launching web app from ' + webAppUrlString);
Expand Down Expand Up @@ -144,7 +171,7 @@ function main() {
electron.protocol.registerFileProtocol(
'outline',
(request, callback) => {
const appPath = new url.URL(request.url).pathname;
const appPath = new URL(request.url).pathname;
const filesystemPath = path.join(__dirname, 'server_manager/web_app', appPath);
callback(filesystemPath);
},
Expand Down
13 changes: 9 additions & 4 deletions src/server_manager/electron_app/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import * as sentry from '@sentry/electron';
import {ipcRenderer} from 'electron';
import {URL} from 'url';

import * as digitalocean_oauth from './digitalocean_oauth';

// For communication between the main and renderer process.
// This file is run in the renderer process *before* nodeIntegration is disabled.
//
// Required since we disable nodeIntegration; for more info, see the entries here for
// nodeIntegration and preload:
// https://electronjs.org/docs/api/browser-window#class-browserwindow
// Use it for main/renderer process communication and configuring Sentry (which works via
// main/renderer process messages).

// DSN is all we need to specify; for all other config - breadcrumbs, etc., see the main process.
const params = new URL(document.URL).searchParams;
sentry.init({dsn: params.get('sentryDsn')});

// tslint:disable-next-line:no-any
(window as any).whitelistCertificate = (fingerprint: string) => {
Expand Down
18 changes: 18 additions & 0 deletions src/server_manager/electron_app/test_action.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash -eu
#
# Copyright 2018 The Outline Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

do_action server_manager/electron_app/build
jasmine --config=$ROOT_DIR/jasmine.json
3 changes: 1 addition & 2 deletions src/server_manager/electron_app/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
]
},
"include": [
"index.ts",
"preload.ts",
"*.ts",
"../types/*.d.ts"
],
"exclude": [
Expand Down
49 changes: 49 additions & 0 deletions src/server_manager/electron_app/util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2018 The Outline Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {throws} from 'assert';

import {redactManagerUrl} from './util';

describe('XHR breadcrumbs', () => {
it('handles the normal case', () => {
expect(redactManagerUrl('https://124.10.10.2:48000/abcd123/access-keys'))
.toEqual('access-keys');
});

it('handles no port', () => {
expect(redactManagerUrl('https://124.10.10.2/abcd123/access-keys')).toEqual('access-keys');
});

it('handles just one path element', () => {
expect(redactManagerUrl('https://124.10.10.2/abcd123')).toEqual('');
expect(redactManagerUrl('https://124.10.10.2/abcd123/')).toEqual('');
});

it('handles no pathname', () => {
expect(redactManagerUrl('https://124.10.10.2')).toEqual('');
expect(redactManagerUrl('https://124.10.10.2/')).toEqual('');
});

it('handles commands with args', () => {
expect(redactManagerUrl('https://124.10.10.2/abcd123/access-keys/52'))
.toEqual('access-keys/52');
});

it('throws on garbage', () => {
throws(() => {
redactManagerUrl('once upon a time');
});
});
});
27 changes: 27 additions & 0 deletions src/server_manager/electron_app/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2018 The Outline Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {URL} from 'url';

// Returns a URL's pathname stripped of its first directory name, which may be the empty string if
// there are fewer than two "directories" in the URL's pathname.
// Throws if s cannot be parsed as a URL.
//
// Used to strip PII from management API URLs, e.g.:
// https://124.10.10.2/abcd123/access-keys -> access-keys
// https://124.10.10.2/abcd123/access-keys/52 -> access-keys/52
// https://124.10.10.2/abcd123 -> (empty string)
export function redactManagerUrl(s: string) {
return new URL(s).pathname.split('/').slice(2).join('/');
}
4 changes: 2 additions & 2 deletions src/server_manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
"email": "info@getoutline.org"
},
"dependencies": {
"@sentry/electron": "^0.8.1",
"body-parser": "^1.18.3",
"bytes": "^3.0.0",
"clipboard-polyfill": "^2.4.6",
"electron-updater": "^2.21.0",
"eventemitter3": "^2.0.3",
"express": "^4.16.3",
"node-forge": "^0.7.1",
"raven-js": "^3.17.0",
"request": "^2.87.0",
"request-lite": "^2.40.1"
},
Expand All @@ -33,4 +33,4 @@
"scripts": {
"postinstall": "bower install"
}
}
}
Loading

0 comments on commit 69cf89c

Please sign in to comment.