diff --git a/.travis.yml b/.travis.yml index eb5c36d0d..f5c1de27f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/jasmine.json b/jasmine.json index f80f1ee8d..26952cc74 100644 --- a/jasmine.json +++ b/jasmine.json @@ -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, diff --git a/src/server_manager/cloud/digitalocean_api.ts b/src/server_manager/cloud/digitalocean_api.ts index f67de44a5..fc5505275 100644 --- a/src/server_manager/cloud/digitalocean_api.ts +++ b/src/server_manager/cloud/digitalocean_api.ts @@ -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; @@ -102,7 +101,7 @@ class RestApiSession implements DigitalOceanSession { constructor(public accessToken: string) {} public getAccount(): Promise { - SentryErrorReporter.logInfo('Requesting account'); + console.info('Requesting account'); return this.request<{account: Account}>('GET', 'account/').then((response) => { return response.account; }); @@ -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, @@ -158,12 +157,12 @@ class RestApiSession implements DigitalOceanSession { } public deleteDroplet(dropletId: number): Promise { - SentryErrorReporter.logInfo('Requesting droplet deletion'); + console.info('Requesting droplet deletion'); return this.request('DELETE', 'droplets/' + dropletId); } public getRegionInfo(): Promise { - SentryErrorReporter.logInfo('Requesting region info'); + console.info('Requesting region info'); return this.request<{regions: RegionInfo[]}>('GET', 'regions').then((response) => { return response.regions; }); @@ -171,7 +170,7 @@ class RestApiSession implements DigitalOceanSession { // Registers a SSH key with DigitalOcean. private registerKey_(keyName: string, publicKeyForSSH: string): Promise { - 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}) @@ -181,7 +180,7 @@ class RestApiSession implements DigitalOceanSession { } public getDroplet(dropletId: number): Promise { - SentryErrorReporter.logInfo('Requesting droplet'); + console.info('Requesting droplet'); return this.request<{droplet: DropletInfo}>('GET', 'droplets/' + dropletId).then((response) => { return response.droplet; }); @@ -194,7 +193,7 @@ class RestApiSession implements DigitalOceanSession { } public getDropletsByTag(tag: string): Promise { - 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; @@ -202,7 +201,7 @@ class RestApiSession implements DigitalOceanSession { } public getDroplets(): Promise { - SentryErrorReporter.logInfo('Requesting droplets'); + console.info('Requesting droplets'); return this.request<{droplets: DropletInfo[]}>('GET', 'droplets/').then((response) => { return response.droplets; }); @@ -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}`)); } @@ -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); diff --git a/src/server_manager/electron_app/index.ts b/src/server_manager/electron_app/index.ts index a95de6101..116437485 100644 --- a/src/server_manager/electron_app/index.ts +++ b/src/server_manager/electron_app/index.ts @@ -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; @@ -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, @@ -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. @@ -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); @@ -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); }, diff --git a/src/server_manager/electron_app/preload.ts b/src/server_manager/electron_app/preload.ts index 2f9958caa..9e6591097 100644 --- a/src/server_manager/electron_app/preload.ts +++ b/src/server_manager/electron_app/preload.ts @@ -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) => { diff --git a/src/server_manager/electron_app/test_action.sh b/src/server_manager/electron_app/test_action.sh new file mode 100755 index 000000000..af6cef027 --- /dev/null +++ b/src/server_manager/electron_app/test_action.sh @@ -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 diff --git a/src/server_manager/electron_app/tsconfig.json b/src/server_manager/electron_app/tsconfig.json index 7e7cefb0a..487d666a0 100644 --- a/src/server_manager/electron_app/tsconfig.json +++ b/src/server_manager/electron_app/tsconfig.json @@ -11,8 +11,7 @@ ] }, "include": [ - "index.ts", - "preload.ts", + "*.ts", "../types/*.d.ts" ], "exclude": [ diff --git a/src/server_manager/electron_app/util.spec.ts b/src/server_manager/electron_app/util.spec.ts new file mode 100644 index 000000000..3d740e4db --- /dev/null +++ b/src/server_manager/electron_app/util.spec.ts @@ -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'); + }); + }); +}); diff --git a/src/server_manager/electron_app/util.ts b/src/server_manager/electron_app/util.ts new file mode 100644 index 000000000..9f5202783 --- /dev/null +++ b/src/server_manager/electron_app/util.ts @@ -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('/'); +} diff --git a/src/server_manager/package.json b/src/server_manager/package.json index 33378ccbe..2ea6d41b2 100644 --- a/src/server_manager/package.json +++ b/src/server_manager/package.json @@ -9,6 +9,7 @@ "email": "info@getoutline.org" }, "dependencies": { + "@sentry/electron": "^0.8.1", "body-parser": "^1.18.3", "bytes": "^3.0.0", "clipboard-polyfill": "^2.4.6", @@ -16,7 +17,6 @@ "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" }, @@ -33,4 +33,4 @@ "scripts": { "postinstall": "bower install" } -} +} \ No newline at end of file diff --git a/src/server_manager/web_app/app.ts b/src/server_manager/web_app/app.ts index 0c8a462f3..21ab9eacb 100644 --- a/src/server_manager/web_app/app.ts +++ b/src/server_manager/web_app/app.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import * as sentry from '@sentry/electron'; import * as events from 'events'; import * as digitalocean_api from '../cloud/digitalocean_api'; @@ -20,8 +21,6 @@ import * as server from '../model/server'; import {TokenManager} from './digitalocean_oauth'; import * as digitalocean_server from './digitalocean_server'; -import {SentryErrorReporter} from './error_reporter'; -import {ManualServerRepository} from './manual_server'; // tslint:disable-next-line:no-any type Polymer = HTMLElement&any; @@ -160,7 +159,11 @@ export class App { appRoot.addEventListener('SubmitFeedback', (event: PolymerEvent) => { const detail = event.detail; try { - SentryErrorReporter.report(detail.userFeedback, detail.feedbackCategory, detail.userEmail); + sentry.captureEvent({ + message: detail.userFeedback, + user: {email: detail.userEmail}, + tags: {category: detail.feedbackCategory} + }); appRoot.showNotification('Thanks for helping us improve! We love hearing from you.'); } catch (e) { appRoot.showError('Failed to submit feedback. Please try again.'); @@ -265,9 +268,7 @@ export class App { } }) .catch((e) => { - const msg = 'Could not fetch server list from DigitalOcean'; - console.error(msg, e); - SentryErrorReporter.logError(msg); + console.error('Could not fetch server list from DigitalOcean'); this.showIntro(); }); } else { @@ -348,7 +349,7 @@ export class App { private displayError(message: string, cause: Error) { console.error(`${message}: ${cause}`); this.appRoot.showError(message); - SentryErrorReporter.logError(message); + console.error(message); } private displayNotification(message: string) { @@ -453,26 +454,24 @@ export class App { }) .catch((e) => { if (e instanceof errors.DeletedServerError) { - // The user deleted this server, no need to show an error or delete - // it again. + // The user deleted this server, no need to show an error or delete it again. return; } const errorMessage = managedServer.isInstallCompleted() ? 'We are unable to connect to your Outline server at the moment. This may be due to a firewall on your network or temporary connectivity issues with digitalocean.com.' : 'There was an error creating your Outline server. This may be due to a firewall on your network or temporary connectivity issues with digitalocean.com.'; - SentryErrorReporter.logError(errorMessage); this.appRoot .showModalDialog( null, // Don't display any title. errorMessage, ['Delete this server', 'Try again']) .then((clickedButtonIndex: number) => { if (clickedButtonIndex === 0) { // user clicked 'Delete this server' - SentryErrorReporter.logInfo('Deleting unreachable server'); + console.info('Deleting unreachable server'); managedServer.getHost().delete().then(() => { this.showCreateServer(); }); } else if (clickedButtonIndex === 1) { // user clicked 'Try again'. - SentryErrorReporter.logInfo('Retrying unreachable server'); + console.info('Retrying unreachable server'); this.showManagedServer(managedServer, true); } }); @@ -492,29 +491,11 @@ export class App { .catch((e) => { // Sanity check - this error is not expected to occur, as showManagedServer // has it's own error handling. - console.error('error from showManagedServer', e); + console.error('error from showManagedServer'); return Promise.reject(e); }); } - // Displays `DIGITAL_OCEAN_CREATION_ERROR_MESSAGE` in a dialog that prompts the user to submit - // feedback. Logs `msg` and `error` to the console and Sentry. - private handleServerCreationFailure(msg: string, error: Error) { - console.error(msg, error); - SentryErrorReporter.logError(msg); - this.appRoot - .showModalDialog( - 'Failed to create server', DIGITAL_OCEAN_CREATION_ERROR_MESSAGE, - ['Cancel', 'Submit Feedback']) - .then((clickedButtonIndex: number) => { - if (clickedButtonIndex === 1) { - const feedbackDialog = this.appRoot.$.feedbackDialog; - feedbackDialog.open(null, null, feedbackDialog.feedbackCategories.INSTALLATION); - } - this.showCreateServer(); // Reset UI. - }); - } - // Show the server management screen. private showServer(selectedServer: server.Server): void { this.selectedServer = selectedServer; @@ -673,16 +654,16 @@ export class App { userInputConfig = userInputConfig.substr(0, userInputConfig.lastIndexOf('}') + 1); serverConfig = JSON.parse(userInputConfig); } catch (e) { - SentryErrorReporter.logError('Invalid server configuration: could not parse JSON.'); + console.error('Invalid server configuration: could not parse JSON.'); return Promise.reject(new Error('')); } if (!serverConfig.apiUrl) { const msg = 'Invalid server configuration: apiUrl is missing.'; - SentryErrorReporter.logError(msg); + console.error(msg); return Promise.reject(new Error(msg)); } else if (!serverConfig.certSha256) { const msg = 'Invalid server configuration: certSha256 is missing.'; - SentryErrorReporter.logError(msg); + console.error(msg); return Promise.reject(new Error(msg)); } @@ -694,7 +675,7 @@ export class App { } else { // Remove inaccessible manual server from local storage. manualServer.forget(); - SentryErrorReporter.logError('Manual server installed but unreachable.'); + console.error('Manual server installed but unreachable.'); return Promise.reject(new errors.UnreachableServerError( 'Your Outline Server was installed correctly, but we are not able to connect to it. Most likely this is because your server\'s firewall rules are blocking incoming connections. Please review them and make sure to allow incoming TCP connections on ports ranging from 1024 to 65535.')); } @@ -717,7 +698,7 @@ export class App { const serverToDelete = this.selectedServer; if (!isManagedServer(serverToDelete)) { const msg = 'cannot delete non-ManagedServer'; - SentryErrorReporter.logError(msg); + console.error(msg); throw new Error(msg); } @@ -748,7 +729,7 @@ export class App { const serverToForget = this.selectedServer; if (!isManualServer(serverToForget)) { const msg = 'cannot forget non-ManualServer'; - SentryErrorReporter.logError(msg); + console.error(msg); throw new Error(msg); } @@ -789,7 +770,7 @@ export class App { private cancelServerCreation(serverToCancel: server.Server): void { if (!isManagedServer(serverToCancel)) { const msg = 'cannot cancel non-ManagedServer'; - SentryErrorReporter.logError(msg); + console.error(msg); throw new Error(msg); } serverToCancel.getHost().delete().then(() => { diff --git a/src/server_manager/web_app/digitalocean_oauth.ts b/src/server_manager/web_app/digitalocean_oauth.ts index 059bbd089..1e9d5ca62 100644 --- a/src/server_manager/web_app/digitalocean_oauth.ts +++ b/src/server_manager/web_app/digitalocean_oauth.ts @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {SentryErrorReporter} from './error_reporter'; - export interface TokenManager { // Returns the Oauth token, or null if unavailable. getStoredToken(): string; @@ -23,7 +21,6 @@ export interface TokenManager { removeTokenFromStorage(): void; } - // TODO: this class combines URL manipulation with persistence logic. // Consider moving the URL manipulation logic to a separate class, so we // can pass in other implementations when the global "window" is not present. @@ -36,9 +33,7 @@ export class DigitalOceanTokenManager implements TokenManager { getStoredToken(): string { const tokenFromStorage = this.getTokenFromStorage(); if (tokenFromStorage) { - const msg = 'found access token in local storage'; - console.log(msg); - SentryErrorReporter.logInfo(msg); + console.info('found access token in local storage'); return tokenFromStorage; } diff --git a/src/server_manager/web_app/digitalocean_server.ts b/src/server_manager/web_app/digitalocean_server.ts index e71980579..61664cdc1 100644 --- a/src/server_manager/web_app/digitalocean_server.ts +++ b/src/server_manager/web_app/digitalocean_server.ts @@ -21,7 +21,6 @@ import {asciiToHex, hexToString} from '../infrastructure/hex_encoding'; import * as do_install_script from '../install_scripts/do_install_script'; import * as server from '../model/server'; -import {SentryErrorReporter} from './error_reporter'; import {ShadowboxServer} from './shadowbox_server'; // WARNING: these strings must be lowercase due to a DigitalOcean case @@ -89,16 +88,14 @@ class DigitaloceanServer extends ShadowboxServer implements server.ManagedServer // Consider passing a RestEndpoint object to the parent constructor, // to better encapsulate the management api address logic. super(); - const msg = 'DigitalOceanServer created'; - console.info(`${msg}: %O`, dropletInfo); - SentryErrorReporter.logInfo(msg); + console.info('DigitalOceanServer created'); this.eventQueue.once('server-active', () => console.timeEnd('activeServer')); this.waitOnInstall(true) .then(() => { this.setInstallCompleted(); }) .catch((e) => { - console.error('Error installing server', e); + console.error(`error installing server: ${e.message}`); }); } @@ -129,7 +126,8 @@ class DigitaloceanServer extends ShadowboxServer implements server.ManagedServer // Server has been installed (Api Url and Certificate have been) // set, but is not healthy. This could occur if the server // is behind a firewall. - SentryErrorReporter.logError('digitalocean_server: Server is unreachable, possibly due to firewall.'); + console.error( + 'digitalocean_server: Server is unreachable, possibly due to firewall.'); reject(new errors.UnreachableServerError()); } }); @@ -156,15 +154,15 @@ class DigitaloceanServer extends ShadowboxServer implements server.ManagedServer return; } if (this.getTagValue(INSTALL_ERROR_TAG)) { - SentryErrorReporter.logError('digitalocean_server: Got error tag ' + this.getTagValue(INSTALL_ERROR_TAG)); + console.error(`error tag: ${this.getTagValue(INSTALL_ERROR_TAG)}`); this.installState = InstallState.ERROR; } else if (Date.now() - startTimestamp >= TIMEOUT_MS) { - SentryErrorReporter.logError('digitalocean_server: hit timeout while waiting for installation'); + console.error('hit timeout while waiting for installation'); this.installState = InstallState.ERROR; } else if (this.setApiUrlAndCertificate()) { // API Url and Certificate have been set, so we have successfully // installed the server and can now make API calls. - SentryErrorReporter.logInfo('digitalocean_server: Successfully found API and cert tags'); + console.info('digitalocean_server: Successfully found API and cert tags'); this.installState = InstallState.SUCCESS; } }; @@ -241,9 +239,7 @@ class DigitaloceanServer extends ShadowboxServer implements server.ManagedServer try { return hexToString(encodedData); } catch (e) { - const msg = 'error decoding hex string'; - console.error(msg, e); - SentryErrorReporter.logError(msg); + console.error('error decoding hex string'); return null; } } @@ -385,7 +381,8 @@ export class DigitaloceanServerRepository implements server.ManagedServerReposit const onceKeyPair = crypto.generateKeyPair(); const watchtowerRefreshSeconds = this.image ? 30 : undefined; const installCommand = getInstallScript( - this.digitalOcean.accessToken, name, this.image, watchtowerRefreshSeconds, this.metricsUrl, this.sentryApiUrl); + this.digitalOcean.accessToken, name, this.image, watchtowerRefreshSeconds, this.metricsUrl, + this.sentryApiUrl); const dropletSpec = { installCommand, @@ -393,24 +390,24 @@ export class DigitaloceanServerRepository implements server.ManagedServerReposit image: 'docker', tags: [SHADOWBOX_TAG], }; - return onceKeyPair.then((keyPair) => { - if (this.debugMode) { - // Strip carriage returns. They produce annoying blank lines when pasting - // into a terminal. - console.log( - `private key for SSH access to new droplet:\n${ - keyPair.private.replace(/\r/g, '')}\n\n` + - 'Use "ssh -i keyfile root@[ip_address]" to connect to the machine'); - } - return this.digitalOcean.createDroplet(name, region, keyPair.public, dropletSpec); - }).then((response) => { - return new DigitaloceanServer(this.digitalOcean, response.droplet); - }); + return onceKeyPair + .then((keyPair) => { + if (this.debugMode) { + // Strip carriage returns, which produce weird blank lines when pasted into a terminal. + console.debug( + `private key for SSH access to new droplet:\n${ + keyPair.private.replace(/\r/g, '')}\n\n` + + 'Use "ssh -i keyfile root@[ip_address]" to connect to the machine'); + } + return this.digitalOcean.createDroplet(name, region, keyPair.public, dropletSpec); + }) + .then((response) => { + return new DigitaloceanServer(this.digitalOcean, response.droplet); + }); } listServers(): Promise { return this.digitalOcean.getDropletsByTag(SHADOWBOX_TAG).then((droplets) => { - console.log('Found droplets: ', droplets); return droplets.map((droplet) => { return new DigitaloceanServer(this.digitalOcean, droplet); }); @@ -436,7 +433,9 @@ function getInstallScript( return '#!/bin/bash -eu\n' + `export DO_ACCESS_TOKEN=${sanitizezedAccessToken}\n` + (image ? `export SB_IMAGE=${image}\n` : '') + - (watchtowerRefreshSeconds ? `export WATCHTOWER_REFRESH_SECONDS=${watchtowerRefreshSeconds}\n` : '') + + (watchtowerRefreshSeconds ? + `export WATCHTOWER_REFRESH_SECONDS=${watchtowerRefreshSeconds}\n` : + '') + (sentryApiUrl ? `export SENTRY_API_URL="${sentryApiUrl}"\n` : '') + (metricsUrl ? `export SB_METRICS_URL=${metricsUrl}\n` : '') + `export SB_DEFAULT_SERVER_NAME="${name}"\n` + do_install_script.SCRIPT; diff --git a/src/server_manager/web_app/error_reporter.ts b/src/server_manager/web_app/error_reporter.ts deleted file mode 100644 index eaffe8f05..000000000 --- a/src/server_manager/web_app/error_reporter.ts +++ /dev/null @@ -1,80 +0,0 @@ -// 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 * as Raven from 'raven-js'; - -import * as errors from '../infrastructure/errors'; - -// TODO(dborkan): This class contains a lot of duplication from the client but -// has the Cordova specific logic removed. Consider combining -// these into 1 shared library if possible. -// tslint:disable-next-line:no-namespace -export namespace SentryErrorReporter { - export class IllegalStateError extends errors.OutlineError { - constructor(message?: string) { - super(message); - } - } - - export function init(sentryDsn: string, appVersion: string): void { - if (Raven.isSetup()) { - throw new IllegalStateError('Error reporter already initialized.'); - } - // Breadcrumbs for console logging and XHR may include PII such as the server IP address, - // secret API prefix, or shadowsocks access credentials. Only enable DOM breadcrumbs to receive - // UI click data. - const autoBreadcrumbOptions = { - dom: true, - console: false, - location: false, - xhr: false, - }; - Raven.config(sentryDsn, {autoBreadcrumbs: autoBreadcrumbOptions, release: appVersion}) - .install(); - try { - // tslint:disable-next-line:no-any - window.addEventListener('unhandledrejection', (event: any) => { - Raven.captureException(event.reason); - }); - } catch (e) { - // window.addEventListener not available, i.e. not running in a browser - // environment. - // TODO: refactor this code so the try/catch isn't necessary and the - // unhandledrejection listener can be tested. - } - } - - export function report(userFeedback: string, feedbackCategory: string, userEmail?: string): void { - if (!Raven.isSetup()) { - throw new IllegalStateError('Error reporter not initialized.'); - } - Raven.setUserContext({email: userEmail || ''}); - Raven.captureMessage(userFeedback, {tags: {category: feedbackCategory}}); - Raven.setUserContext(); // Reset the user context, don't cache the email - } - - // Logs an info message to be sent to Sentry when `report` is called. - export function logInfo(message: string): void { - log({message, level: 'info'}); - } - - // Logs an error message to be sent to Sentry when `report` is called. - export function logError(message: string): void { - log({message, level: 'error'}); - } - - function log(breadcrumb: Raven.Breadcrumb) { - Raven.captureBreadcrumb(breadcrumb); - } -} diff --git a/src/server_manager/web_app/main.ts b/src/server_manager/web_app/main.ts index b1031dbfa..02f69ff16 100644 --- a/src/server_manager/web_app/main.ts +++ b/src/server_manager/web_app/main.ts @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// A little bit of Node.js that is available to us thanks to Browserify. import * as url from 'url'; import * as digitalocean_api from '../cloud/digitalocean_api'; @@ -20,11 +19,8 @@ import * as digitalocean_api from '../cloud/digitalocean_api'; import {App} from './app'; import {DigitalOceanTokenManager} from './digitalocean_oauth'; import * as digitalocean_server from './digitalocean_server'; -import {SentryErrorReporter} from './error_reporter'; import {ManualServerRepository} from './manual_server'; -const DEFAULT_SENTRY_DSN = 'https://533e56d1b2d64314bd6092a574e6d0f1@sentry.io/215496'; - function ensureString(queryParam: string|string[]): string { if (Array.isArray(queryParam)) { // We pick the last one if the parameter appears multiple times. @@ -41,10 +37,7 @@ document.addEventListener('WebComponentsReady', () => { const metricsUrl = ensureString(queryParams.metricsUrl); const shadowboxImage = ensureString(queryParams.image); const version = ensureString(queryParams.version); - const sentryDsn = ensureString(queryParams.sentryDsn) || DEFAULT_SENTRY_DSN; - - // Initialize error reporting. - SentryErrorReporter.init(sentryDsn, version); + const sentryDsn = ensureString(queryParams.sentryDsn); // Set DigitalOcean server repository parameters. const digitalOceanServerRepositoryFactory = (session: digitalocean_api.DigitalOceanSession) => { diff --git a/src/server_manager/web_app/manual_server.ts b/src/server_manager/web_app/manual_server.ts index 4ab69b660..7d4abaff0 100644 --- a/src/server_manager/web_app/manual_server.ts +++ b/src/server_manager/web_app/manual_server.ts @@ -15,7 +15,6 @@ import {hexToString} from '../infrastructure/hex_encoding'; import * as server from '../model/server'; -import {SentryErrorReporter} from './error_reporter'; import {ShadowboxServer} from './shadowbox_server'; class ManualServer extends ShadowboxServer implements server.ManualServer { @@ -29,9 +28,7 @@ class ManualServer extends ShadowboxServer implements server.ManualServer { whitelistCertificate(btoa(hexToString(config.certSha256))); } catch (e) { // Error whitelisting certificate, may be due to bad user input. - const msg = 'Error whitelisting certificate'; - console.error(msg, e); - SentryErrorReporter.logError(msg); + console.error('Error whitelisting certificate'); } } @@ -61,9 +58,7 @@ export class ManualServerRepository implements server.ManualServerRepository { }); return Promise.resolve(manualServers); } catch (e) { - const msg = 'Error creating manual servers from localStorage'; - console.error(msg, e); - SentryErrorReporter.logError(msg); + console.error('Error creating manual servers from localStorage'); } } return Promise.resolve([]); diff --git a/src/server_manager/web_app/shadowbox_server.ts b/src/server_manager/web_app/shadowbox_server.ts index 50c109a84..dfcb4476a 100644 --- a/src/server_manager/web_app/shadowbox_server.ts +++ b/src/server_manager/web_app/shadowbox_server.ts @@ -14,7 +14,6 @@ import * as errors from '../infrastructure/errors'; import * as server from '../model/server'; -import {SentryErrorReporter} from './error_reporter'; // Interfaces used by metrics REST APIs. interface MetricsEnabled { @@ -37,31 +36,30 @@ export class ShadowboxServer implements server.Server { constructor() {} listAccessKeys(): Promise { - SentryErrorReporter.logInfo('Listing access keys'); + console.info('Listing access keys'); return this.apiRequest<{accessKeys: server.AccessKey[]}>('access-keys').then((response) => { return response.accessKeys; }); } addAccessKey(): Promise { - SentryErrorReporter.logInfo('Adding access key'); + console.info('Adding access key'); return this.apiRequest('access-keys', {method: 'POST'}); } renameAccessKey(accessKeyId: server.AccessKeyId, name: string): Promise { - SentryErrorReporter.logInfo('Renaming access key'); + console.info('Renaming access key'); const body = new FormData(); body.append('name', name); return this.apiRequest('access-keys/' + accessKeyId + '/name', {method: 'PUT', body}); } removeAccessKey(accessKeyId: server.AccessKeyId): Promise { - SentryErrorReporter.logInfo('Removing access key'); + console.info('Removing access key'); return this.apiRequest('access-keys/' + accessKeyId, {method: 'DELETE'}); } getDataUsage(): Promise { - SentryErrorReporter.logInfo('Retrieving data usage'); return this.apiRequest('metrics/transfer'); } @@ -70,7 +68,7 @@ export class ShadowboxServer implements server.Server { } setName(name: string): Promise { - SentryErrorReporter.logInfo('Setting server name'); + console.info('Setting server name'); const requestOptions: RequestInit = { method: 'PUT', headers: new Headers({'Content-Type': 'application/json'}), @@ -87,7 +85,7 @@ export class ShadowboxServer implements server.Server { setMetricsEnabled(metricsEnabled: boolean): Promise { const action = metricsEnabled ? 'Enabling' : 'Disabling'; - SentryErrorReporter.logInfo(`${action} metrics`); + console.info(`${action} metrics`); const requestOptions: RequestInit = { method: 'PUT', headers: new Headers({'Content-Type': 'application/json'}), @@ -142,7 +140,7 @@ export class ShadowboxServer implements server.Server { } private getServerConfig(): Promise { - SentryErrorReporter.logInfo('Retrieving server configuration'); + console.info('Retrieving server configuration'); return this.apiRequest('server'); } @@ -156,7 +154,7 @@ export class ShadowboxServer implements server.Server { let apiAddress = this.managementApiAddress; if (!apiAddress) { const msg = 'Management API address unavailable'; - SentryErrorReporter.logError(msg); + console.error(msg); throw new Error(msg); } if (!apiAddress.endsWith('/')) { @@ -167,19 +165,14 @@ export class ShadowboxServer implements server.Server { .then( (response) => { if (!response.ok) { - const msg = `API request to ${path} failed with status ${response.status}`; - console.error(msg); - SentryErrorReporter.logError(msg); - throw new errors.ServerApiError(msg, response); + throw new errors.ServerApiError( + `API request to ${path} failed with status ${response.status}`, response); } - console.debug(`API request to ${path} succeeded`); return response.text(); }, (error) => { - const msg = `API request to ${path} failed due to network error`; - console.error(msg, error); - SentryErrorReporter.logError(msg); - throw new errors.ServerApiError(msg); + throw new errors.ServerApiError( + `API request to ${path} failed due to network error`); }) .then((body) => { if (!body) { diff --git a/yarn.lock b/yarn.lock index c6f827533..65065d658 100644 --- a/yarn.lock +++ b/yarn.lock @@ -109,6 +109,78 @@ string-format-obj "^1.0.0" through2 "^2.0.0" +"@sentry/browser@4.0.0-beta.12": + version "4.0.0-beta.12" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-4.0.0-beta.12.tgz#dff44c7a3732577057844b1643e0ba38c644138b" + dependencies: + "@sentry/core" "4.0.0-beta.12" + "@sentry/hub" "4.0.0-beta.12" + "@sentry/minimal" "4.0.0-beta.12" + "@sentry/types" "4.0.0-beta.12" + "@sentry/utils" "4.0.0-beta.12" + +"@sentry/core@4.0.0-beta.12": + version "4.0.0-beta.12" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-4.0.0-beta.12.tgz#c821e41b02c1d66e48fbef16da744f0173575558" + dependencies: + "@sentry/hub" "4.0.0-beta.12" + "@sentry/minimal" "4.0.0-beta.12" + "@sentry/types" "4.0.0-beta.12" + "@sentry/utils" "4.0.0-beta.12" + +"@sentry/electron@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@sentry/electron/-/electron-0.8.1.tgz#0b978efee79697d3bb86cce7f906edbea98fd6a9" + dependencies: + "@sentry/browser" "4.0.0-beta.12" + "@sentry/core" "4.0.0-beta.12" + "@sentry/hub" "4.0.0-beta.12" + "@sentry/minimal" "4.0.0-beta.12" + "@sentry/node" "4.0.0-beta.12" + "@sentry/types" "4.0.0-beta.12" + "@sentry/utils" "4.0.0-beta.12" + electron-fetch "^1.1.0" + form-data "^2.3.2" + util.promisify "^1.0.0" + +"@sentry/hub@4.0.0-beta.12": + version "4.0.0-beta.12" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-4.0.0-beta.12.tgz#85267cec47c0bbf1094a537f7d14d19495c77234" + dependencies: + "@sentry/types" "4.0.0-beta.12" + "@sentry/utils" "4.0.0-beta.12" + +"@sentry/minimal@4.0.0-beta.12": + version "4.0.0-beta.12" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-4.0.0-beta.12.tgz#534e8edd065646e0e5f8d71443a63f6ce7187573" + dependencies: + "@sentry/hub" "4.0.0-beta.12" + "@sentry/types" "4.0.0-beta.12" + +"@sentry/node@4.0.0-beta.12": + version "4.0.0-beta.12" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-4.0.0-beta.12.tgz#efc5b61ebde118a2a22e15530ddac16eb804d639" + dependencies: + "@sentry/core" "4.0.0-beta.12" + "@sentry/hub" "4.0.0-beta.12" + "@sentry/minimal" "4.0.0-beta.12" + "@sentry/types" "4.0.0-beta.12" + "@sentry/utils" "4.0.0-beta.12" + cookie "0.3.1" + lsmod "1.0.0" + md5 "2.2.1" + stack-trace "0.0.10" + +"@sentry/types@4.0.0-beta.12": + version "4.0.0-beta.12" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-4.0.0-beta.12.tgz#0abd303692e48c0fc11afbfea8cbad87e625357a" + +"@sentry/utils@4.0.0-beta.12": + version "4.0.0-beta.12" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-4.0.0-beta.12.tgz#9d51e88634843232b6c6f0edb6df3d3fba83071e" + dependencies: + "@sentry/types" "4.0.0-beta.12" + "@types/body-parser@*": version "1.16.8" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.16.8.tgz#687ec34140624a3bec2b1a8ea9268478ae8f3be3" @@ -973,6 +1045,10 @@ chalk@^2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + chromium-pickle-js@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205" @@ -1222,6 +1298,10 @@ cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" +crypt@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + cryptiles@2.x.x: version "2.0.5" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" @@ -1341,6 +1421,12 @@ deep-extend@~0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" +define-properties@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + dependencies: + object-keys "^1.0.12" + defined@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" @@ -1606,6 +1692,12 @@ electron-download@^3.0.1: semver "^5.3.0" sumchecker "^1.2.0" +electron-fetch@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/electron-fetch/-/electron-fetch-1.2.1.tgz#08a033a23cc47febf05457f3e0e0a26598d02b95" + dependencies: + encoding "^0.1.12" + electron-icon-maker@^0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/electron-icon-maker/-/electron-icon-maker-0.0.4.tgz#0766087c270a736d0857204bb72130d574d91c51" @@ -1679,6 +1771,12 @@ encodeurl@~1.0.1, encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" +encoding@^0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + dependencies: + iconv-lite "~0.4.13" + end-of-stream@^1.0.0, end-of-stream@^1.1.0: version "1.4.1" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" @@ -1699,6 +1797,24 @@ error-ex@^1.2.0: dependencies: is-arrayish "^0.2.1" +es-abstract@^1.5.1: + version "1.12.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" + dependencies: + es-to-primitive "^1.1.1" + function-bind "^1.1.1" + has "^1.0.1" + is-callable "^1.1.3" + is-regex "^1.0.4" + +es-to-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" + dependencies: + is-callable "^1.1.1" + is-date-object "^1.0.1" + is-symbol "^1.0.1" + es6-promise@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.1.tgz#8811e90915d9a0dba36274f0b242dbda78f9c92a" @@ -1950,20 +2066,20 @@ forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" -form-data@~2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" +form-data@^2.3.2, form-data@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" dependencies: asynckit "^0.4.0" - combined-stream "^1.0.5" + combined-stream "1.0.6" mime-types "^2.1.12" -form-data@~2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" +form-data@~2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" dependencies: asynckit "^0.4.0" - combined-stream "1.0.6" + combined-stream "^1.0.5" mime-types "^2.1.12" formidable@^1.0.14: @@ -2059,7 +2175,7 @@ fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: mkdirp ">=0.5 0" rimraf "2" -function-bind@^1.0.2: +function-bind@^1.0.2, function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -2312,6 +2428,12 @@ has@^1.0.0: dependencies: function-bind "^1.0.2" +has@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + dependencies: + function-bind "^1.1.1" + hash-base@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" @@ -2487,7 +2609,7 @@ iconv-lite@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" -iconv-lite@0.4.23, iconv-lite@^0.4.23: +iconv-lite@0.4.23, iconv-lite@^0.4.23, iconv-lite@~0.4.13: version "0.4.23" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" dependencies: @@ -2574,7 +2696,7 @@ is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" -is-buffer@^1.1.0: +is-buffer@^1.1.0, is-buffer@~1.1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -2584,6 +2706,10 @@ is-builtin-module@^1.0.0: dependencies: builtin-modules "^1.0.0" +is-callable@^1.1.1, is-callable@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" + is-ci@^1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.0.10.tgz#f739336b2632365061a9d48270cd56ae3369318e" @@ -2596,6 +2722,10 @@ is-ci@^1.1.0: dependencies: ci-info "^1.0.0" +is-date-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + is-finite@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" @@ -2659,6 +2789,12 @@ is-redirect@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" +is-regex@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" + dependencies: + has "^1.0.1" + is-retry-allowed@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" @@ -2671,6 +2807,10 @@ is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" +is-symbol@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" + is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -2985,6 +3125,10 @@ lru-cache@^4.0.1: pseudomap "^1.0.2" yallist "^2.1.2" +lsmod@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lsmod/-/lsmod-1.0.0.tgz#9a00f76dca36eb23fa05350afe1b585d4299e64b" + make-dir@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b" @@ -3009,6 +3153,14 @@ md5.js@^1.3.4: hash-base "^3.0.0" inherits "^2.0.1" +md5@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -3274,10 +3426,21 @@ object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" +object-keys@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" + object-keys@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" +object.getownpropertydescriptors@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.1" + obuf@^1.0.0, obuf@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" @@ -3748,10 +3911,6 @@ range-parser@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" -raven-js@^3.17.0: - version "3.24.0" - resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.24.0.tgz#59464d8bc4b3812ae87a282e9bb98ecad5b4b047" - raw-body@2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" @@ -4363,7 +4522,7 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" -stack-trace@0.0.x: +stack-trace@0.0.10, stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -4829,6 +4988,13 @@ util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" +util.promisify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" + dependencies: + define-properties "^1.1.2" + object.getownpropertydescriptors "^2.0.3" + util@0.10.3, util@~0.10.1: version "0.10.3" resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"