Commit 542b96f0 authored by Robert Knight's avatar Robert Knight

Replace raven-js with Sentry's new JavaScript SDK

Replace the legacy raven-js package with Sentry's new JS SDK [1], with a
minimal wrapper in `util/sentry.js` that exports the same interface as
the previous `raven.js` module.

To enable Sentry, h or the browser extension must set a `sentry` key in the
sidebar app's configuration.

As part of switching SDKs, the following changes were made:

 - The AngularJS integration was removed, as we are in the process of
   migrating away from Angular
 - The custom code for unhandled promise rejections was removed, as the
   new SDK handles this already
 - The URL rewriting logic was removed. This was done to make the setup
   as minimal as possible. This may need to be re-added later.
 - The `report` function was removed, as it was not used anywhere
 - The `session` module now imports `util/sentry` directly rather than
   using Angular dependency injection. Now that we have a convenient way
   to mock all CJS/ES imports, there isn't a need to use DI for
   utilities

[1] https://docs.sentry.io/platforms/javascript/
parent 5d730afe
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
"@babel/preset-env": "^7.1.6", "@babel/preset-env": "^7.1.6",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@octokit/rest": "^16.9.0", "@octokit/rest": "^16.9.0",
"@sentry/browser": "^5.6.2",
"angular": "^1.7.5", "angular": "^1.7.5",
"angular-mocks": "^1.7.5", "angular-mocks": "^1.7.5",
"angular-route": "^1.7.5", "angular-route": "^1.7.5",
...@@ -93,7 +94,6 @@ ...@@ -93,7 +94,6 @@
"puppeteer": "^1.2.0", "puppeteer": "^1.2.0",
"query-string": "^3.0.1", "query-string": "^3.0.1",
"raf": "^3.1.0", "raf": "^3.1.0",
"raven-js": "^3.7.0",
"redux": "^4.0.1", "redux": "^4.0.1",
"redux-thunk": "^2.1.0", "redux-thunk": "^2.1.0",
"request": "^2.76.0", "request": "^2.76.0",
......
...@@ -11,8 +11,8 @@ module.exports = { ...@@ -11,8 +11,8 @@ module.exports = {
jquery: ['jquery'], jquery: ['jquery'],
angular: ['angular', 'angular-route', 'ng-tags-input', 'angular-toastr'], angular: ['angular', 'angular-route', 'ng-tags-input', 'angular-toastr'],
katex: ['katex'], katex: ['katex'],
sentry: ['@sentry/browser'],
showdown: ['showdown'], showdown: ['showdown'],
raven: ['raven-js'],
}, },
// List of modules to exclude from parsing for require() statements. // List of modules to exclude from parsing for require() statements.
......
...@@ -112,7 +112,7 @@ function bootSidebarApp(doc, config) { ...@@ -112,7 +112,7 @@ function bootSidebarApp(doc, config) {
...polyfills, ...polyfills,
// Vendor code required by sidebar.bundle.js // Vendor code required by sidebar.bundle.js
'scripts/raven.bundle.js', 'scripts/sentry.bundle.js',
'scripts/angular.bundle.js', 'scripts/angular.bundle.js',
'scripts/katex.bundle.js', 'scripts/katex.bundle.js',
'scripts/showdown.bundle.js', 'scripts/showdown.bundle.js',
......
...@@ -41,7 +41,7 @@ describe('bootstrap', function() { ...@@ -41,7 +41,7 @@ describe('bootstrap', function() {
'styles/pdfjs-overrides.css', 'styles/pdfjs-overrides.css',
// Sidebar app // Sidebar app
'scripts/raven.bundle.js', 'scripts/sentry.bundle.js',
'scripts/angular.bundle.js', 'scripts/angular.bundle.js',
'scripts/katex.bundle.js', 'scripts/katex.bundle.js',
'scripts/showdown.bundle.js', 'scripts/showdown.bundle.js',
...@@ -151,7 +151,7 @@ describe('bootstrap', function() { ...@@ -151,7 +151,7 @@ describe('bootstrap', function() {
const expectedAssets = [ const expectedAssets = [
'scripts/angular.bundle.1234.js', 'scripts/angular.bundle.1234.js',
'scripts/katex.bundle.1234.js', 'scripts/katex.bundle.1234.js',
'scripts/raven.bundle.1234.js', 'scripts/sentry.bundle.1234.js',
'scripts/showdown.bundle.1234.js', 'scripts/showdown.bundle.1234.js',
'scripts/sidebar.bundle.1234.js', 'scripts/sidebar.bundle.1234.js',
'styles/angular-csp.1234.css', 'styles/angular-csp.1234.css',
......
...@@ -6,16 +6,16 @@ const { fetchConfig } = require('./util/fetch-config'); ...@@ -6,16 +6,16 @@ const { fetchConfig } = require('./util/fetch-config');
const serviceConfig = require('./service-config'); const serviceConfig = require('./service-config');
const crossOriginRPC = require('./cross-origin-rpc.js'); const crossOriginRPC = require('./cross-origin-rpc.js');
let raven; let sentry;
// Read settings rendered into sidebar app HTML by service/extension. // Read settings rendered into sidebar app HTML by service/extension.
const appConfig = require('../shared/settings').jsonConfigsFrom(document); const appConfig = require('../shared/settings').jsonConfigsFrom(document);
if (appConfig.raven) { if (appConfig.sentry) {
// Initialize Raven. This is required at the top of this file // Initialize Sentry. This is required at the top of this file
// so that it happens early in the app's startup flow // so that it happens early in the app's startup flow
raven = require('./raven'); sentry = require('./util/sentry');
raven.init(appConfig.raven); sentry.init(appConfig.sentry);
} }
// Disable Angular features that are not compatible with CSP. // Disable Angular features that are not compatible with CSP.
...@@ -43,13 +43,6 @@ require('preact/debug'); ...@@ -43,13 +43,6 @@ require('preact/debug');
const wrapReactComponent = require('./util/wrap-react-component'); const wrapReactComponent = require('./util/wrap-react-component');
// Setup Angular integration for Raven
if (appConfig.raven) {
raven.angularModule(angular);
} else {
angular.module('ngRaven', []);
}
if (appConfig.googleAnalytics) { if (appConfig.googleAnalytics) {
addAnalytics(appConfig.googleAnalytics); addAnalytics(appConfig.googleAnalytics);
} }
...@@ -130,9 +123,6 @@ function startAngularApp(config) { ...@@ -130,9 +123,6 @@ function startAngularApp(config) {
// Angular addons which do not export the Angular module // Angular addons which do not export the Angular module
// name via module.exports // name via module.exports
['ngTagsInput', require('ng-tags-input')][0], ['ngTagsInput', require('ng-tags-input')][0],
// Local addons
'ngRaven',
]) ])
// The root component for the application // The root component for the application
...@@ -239,7 +229,6 @@ function startAngularApp(config) { ...@@ -239,7 +229,6 @@ function startAngularApp(config) {
.value('VirtualThreadList', require('./virtual-thread-list')) .value('VirtualThreadList', require('./virtual-thread-list'))
.value('isSidebar', isSidebar) .value('isSidebar', isSidebar)
.value('random', require('./util/random')) .value('random', require('./util/random'))
.value('raven', require('./raven'))
.value('serviceConfig', serviceConfig) .value('serviceConfig', serviceConfig)
.value('settings', config) .value('settings', config)
.value('time', require('./util/time')) .value('time', require('./util/time'))
......
'use strict';
/**
* This module configures Raven for reporting crashes
* to Sentry.
*
* Logging requires the Sentry DSN and Hypothesis
* version to be provided via the app's settings object.
*
* It also exports an Angular module via angularModule() which integrates
* error logging into any Angular application that it is added to
* as a dependency.
*/
const Raven = require('raven-js');
const angularPlugin = require('raven-js/plugins/angular');
/**
* Returns the input URL if it is an HTTP URL or the filename part of the URL
* otherwise.
*
* @param {string} url - The script URL associated with an exception stack
* frame.
*/
function convertLocalURLsToFilenames(url) {
if (!url) {
return url;
}
if (url.match(/https?:/)) {
return url;
}
// Strip the query string (which is used as a cache buster)
// and extract the filename from the URL
return url
.replace(/\?.*/, '')
.split('/')
.slice(-1)[0];
}
/**
* Return a transformed version of `data` with local URLs replaced
* with filenames.
*
* In environments where the client is served from a local URL,
* eg. chrome-extension://<ID>/scripts/bundle.js, the script URL
* and the sourcemap it references will not be accessible to Sentry.
*
* Therefore on the client we replace references to such URLs with just
* the filename part and then as part of the release process, upload both
* the source file and the source map to Sentry.
*
* Using just the filename allows us to upload a single set of source files
* and sourcemaps for a release though a given release of H might be served
* from multiple actual URLs (eg. different browser extensions).
*/
function translateSourceURLs(data) {
try {
const frames = data.exception.values[0].stacktrace.frames;
frames.forEach(function(frame) {
frame.filename = convertLocalURLsToFilenames(frame.filename);
});
data.culprit = frames[0].filename;
} catch (err) {
console.warn('Failed to normalize error stack trace', err, data);
}
return data;
}
function init(config) {
Raven.config(config.dsn, {
release: '__VERSION__', // replaced by versionify
dataCallback: translateSourceURLs,
}).install();
installUnhandledPromiseErrorHandler();
}
function setUserInfo(info) {
if (info) {
Raven.setUserContext(info);
} else {
Raven.setUserContext();
}
}
/**
* Initializes and returns the Angular module which provides
* a custom wrapper around Angular's $exceptionHandler service,
* logging any exceptions passed to it using Sentry.
*
* This must be invoked _after_ Raven is configured using init().
*/
function angularModule(angular) {
const prevCallback = Raven._globalOptions.dataCallback;
angularPlugin(Raven, angular);
// Hack: Ensure that both our data callback and the one provided by
// the Angular plugin are run when submitting errors.
//
// The Angular plugin replaces any previously installed
// data callback with its own which does not in turn call the
// previously registered callback that we registered when calling
// Raven.config().
//
// See https://github.com/getsentry/raven-js/issues/522
const angularCallback = Raven._globalOptions.dataCallback;
Raven.setDataCallback(function(data) {
return angularCallback(prevCallback(data));
});
return angular.module('ngRaven');
}
/**
* Report an error to Sentry.
*
* @param {Error} error - An error object describing what went wrong
* @param {string} when - A string describing the context in which
* the error occurred.
* @param {Object} [context] - A JSON-serializable object containing additional
* information which may be useful when
* investigating the error.
*/
function report(error, when, context) {
if (!(error instanceof Error)) {
// If the passed object is not an Error, raven-js
// will serialize it using toString() which produces unhelpful results
// for objects that do not provide their own toString() implementations.
//
// If the error is a plain object or non-Error subclass with a message
// property, such as errors returned by chrome.extension.lastError,
// use that instead.
if (typeof error === 'object' && error.message) {
error = error.message;
}
}
const extra = Object.assign({ when: when }, context);
Raven.captureException(error, { extra: extra });
}
/**
* Installs a handler to catch unhandled rejected promises.
*
* For this to work, the browser or the Promise polyfill must support
* the unhandled promise rejection event (Chrome >= 49). On other browsers,
* the rejections will simply go unnoticed. Therefore, app code _should_
* always provide a .catch() handler on the top-most promise chain.
*
* See https://github.com/getsentry/raven-js/issues/424
* and https://www.chromestatus.com/feature/4805872211460096
*
* It is possible that future versions of Raven JS may handle these events
* automatically, in which case this code can simply be removed.
*/
function installUnhandledPromiseErrorHandler() {
window.addEventListener('unhandledrejection', function(event) {
if (event.reason) {
report(event.reason, 'Unhandled Promise rejection');
}
});
}
module.exports = {
init: init,
angularModule: angularModule,
setUserInfo: setUserInfo,
report: report,
};
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
const events = require('../events'); const events = require('../events');
const retryUtil = require('../util/retry'); const retryUtil = require('../util/retry');
const sentry = require('../util/sentry');
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
...@@ -27,7 +28,6 @@ function session( ...@@ -27,7 +28,6 @@ function session(
api, api,
auth, auth,
flash, flash,
raven,
settings, settings,
serviceConfig serviceConfig
) { ) {
...@@ -122,11 +122,11 @@ function session( ...@@ -122,11 +122,11 @@ function session(
// Associate error reports with the current user in Sentry. // Associate error reports with the current user in Sentry.
if (model.userid) { if (model.userid) {
raven.setUserInfo({ sentry.setUserInfo({
id: model.userid, id: model.userid,
}); });
} else { } else {
raven.setUserInfo(undefined); sentry.setUserInfo(null);
} }
} }
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
const angular = require('angular'); const angular = require('angular');
const events = require('../../events'); const events = require('../../events');
const sessionFactory = require('../session');
const mock = angular.mock; const mock = angular.mock;
...@@ -12,15 +13,17 @@ describe('sidebar.session', function() { ...@@ -12,15 +13,17 @@ describe('sidebar.session', function() {
let fakeAnalytics; let fakeAnalytics;
let fakeAuth; let fakeAuth;
let fakeFlash; let fakeFlash;
let fakeRaven; let fakeSentry;
let fakeServiceConfig; let fakeServiceConfig;
let fakeSettings; let fakeSettings;
let fakeApi; let fakeApi;
let sandbox; let sandbox;
// The instance of the `session` service.
let session; let session;
before(function() { before(function() {
angular.module('h', []).service('session', require('../session')); angular.module('h', []).service('session', sessionFactory);
}); });
beforeEach(function() { beforeEach(function() {
...@@ -43,7 +46,7 @@ describe('sidebar.session', function() { ...@@ -43,7 +46,7 @@ describe('sidebar.session', function() {
login: sandbox.stub().returns(Promise.resolve()), login: sandbox.stub().returns(Promise.resolve()),
}; };
fakeFlash = { error: sandbox.spy() }; fakeFlash = { error: sandbox.spy() };
fakeRaven = { fakeSentry = {
setUserInfo: sandbox.spy(), setUserInfo: sandbox.spy(),
}; };
fakeApi = { fakeApi = {
...@@ -63,10 +66,13 @@ describe('sidebar.session', function() { ...@@ -63,10 +66,13 @@ describe('sidebar.session', function() {
api: fakeApi, api: fakeApi,
auth: fakeAuth, auth: fakeAuth,
flash: fakeFlash, flash: fakeFlash,
raven: fakeRaven,
settings: fakeSettings, settings: fakeSettings,
serviceConfig: fakeServiceConfig, serviceConfig: fakeServiceConfig,
}); });
sessionFactory.$imports.$mock({
'../util/sentry': fakeSentry,
});
}); });
beforeEach( beforeEach(
...@@ -77,6 +83,7 @@ describe('sidebar.session', function() { ...@@ -77,6 +83,7 @@ describe('sidebar.session', function() {
); );
afterEach(function() { afterEach(function() {
sessionFactory.$imports.$restore();
sandbox.restore(); sandbox.restore();
}); });
...@@ -198,7 +205,7 @@ describe('sidebar.session', function() { ...@@ -198,7 +205,7 @@ describe('sidebar.session', function() {
session.update({ session.update({
userid: 'anne', userid: 'anne',
}); });
assert.calledWith(fakeRaven.setUserInfo, { assert.calledWith(fakeSentry.setUserInfo, {
id: 'anne', id: 'anne',
}); });
}); });
......
'use strict';
const raven = require('../raven');
function fakeExceptionData(scriptURL) {
return {
exception: {
values: [
{
stacktrace: {
frames: [
{
filename: scriptURL,
},
],
},
},
],
},
culprit: scriptURL,
};
}
describe('raven', function() {
// A stub for the callback that the Angular plugin installs with
// Raven.setDataCallback()
let fakeAngularTransformer;
let fakeAngularPlugin;
let fakeRavenJS;
beforeEach(function() {
fakeRavenJS = {
config: sinon.stub().returns({
install: sinon.stub(),
}),
captureException: sinon.stub(),
setDataCallback: function(callback) {
this._globalOptions.dataCallback = callback;
},
_globalOptions: {
dataCallback: undefined,
},
};
fakeAngularTransformer = sinon.stub();
fakeAngularPlugin = sinon.spy(function(Raven) {
Raven.setDataCallback(fakeAngularTransformer);
});
raven.$imports.$mock({
'raven-js': fakeRavenJS,
'raven-js/plugins/angular': fakeAngularPlugin,
});
});
afterEach(() => {
raven.$imports.$restore();
});
describe('.install()', function() {
it('installs a handler for uncaught promises', function() {
raven.init({
dsn: 'dsn',
release: 'release',
});
const event = document.createEvent('Event');
event.initEvent(
'unhandledrejection',
true /* bubbles */,
true /* cancelable */
);
event.reason = new Error('Some error');
window.dispatchEvent(event);
assert.calledWith(
fakeRavenJS.captureException,
event.reason,
sinon.match.any
);
});
});
describe('pre-submission data transformation', function() {
let dataCallback;
beforeEach(function() {
raven.init({ dsn: 'dsn', release: 'release' });
const configOpts = fakeRavenJS.config.args[0][1];
dataCallback = configOpts && configOpts.dataCallback;
});
it('installs a transformer', function() {
assert.ok(dataCallback);
});
it('replaces non-HTTP URLs with filenames', function() {
const scriptURL = 'chrome-extension://1234/public/bundle.js';
const transformed = dataCallback(fakeExceptionData(scriptURL));
assert.equal(transformed.culprit, 'bundle.js');
const transformedStack = transformed.exception.values[0].stacktrace;
assert.equal(transformedStack.frames[0].filename, 'bundle.js');
});
it('does not modify HTTP URLs', function() {
const scriptURL = 'https://hypothes.is/assets/scripts/bundle.js';
const transformed = dataCallback(fakeExceptionData(scriptURL));
assert.equal(transformed.culprit, scriptURL);
const transformedStack = transformed.exception.values[0].stacktrace;
assert.equal(transformedStack.frames[0].filename, scriptURL);
});
});
describe('.report()', function() {
it('extracts the message property from Error-like objects', function() {
raven.report({ message: 'An error' }, 'context');
assert.calledWith(fakeRavenJS.captureException, 'An error', {
extra: {
when: 'context',
},
});
});
it('passes extra details through', function() {
const error = new Error('an error');
raven.report(error, 'some operation', { url: 'foobar.com' });
assert.calledWith(fakeRavenJS.captureException, error, {
extra: {
when: 'some operation',
url: 'foobar.com',
},
});
});
});
describe('.angularModule()', function() {
let angularStub;
beforeEach(function() {
angularStub = {
module: sinon.stub(),
};
});
it('installs the Angular plugin', function() {
raven.init('dsn');
raven.angularModule(angularStub);
assert.calledWith(fakeAngularPlugin, fakeRavenJS, angularStub);
});
it('installs the data transformers', function() {
raven.init('dsn');
const originalTransformer = sinon.stub();
fakeRavenJS._globalOptions.dataCallback = originalTransformer;
raven.angularModule(angularStub);
fakeRavenJS._globalOptions.dataCallback(
fakeExceptionData('app.bundle.js')
);
// Check that both our data transformer and the one provided by
// the Angular plugin were invoked
assert.called(originalTransformer);
assert.called(fakeAngularTransformer);
});
});
});
'use strict';
const Sentry = require('@sentry/browser');
/**
* @typedef SentryConfig
* @prop {string} dsn
*/
/**
* Initialize the Sentry integration.
*
* This will activate Sentry and enable capturing of uncaught errors and
* unhandled promise rejections.
*
* @param {SentryConfig} config
*/
function init(config) {
Sentry.init({
dsn: config.dsn,
release: '__VERSION__', // replaced by versionify
});
}
/**
* Record the user ID of the logged-in user.
*
* See https://docs.sentry.io/platforms/javascript/#capturing-the-user
*
* @param {import('@sentry/browser').User|null} user
*/
function setUserInfo(user) {
Sentry.setUser(user);
}
module.exports = {
init,
setUserInfo,
};
...@@ -765,6 +765,58 @@ ...@@ -765,6 +765,58 @@
universal-user-agent "^3.0.0" universal-user-agent "^3.0.0"
url-template "^2.0.8" url-template "^2.0.8"
"@sentry/browser@^5.6.2":
version "5.6.2"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.6.2.tgz#f39e95c3aff2d4b4fd5d0d4aa7192af73f20d24e"
integrity sha512-Nm/W/5ra6+OQCWQkdd86vHjcYUjHCVqCzQyPasd6HE7SNlWe5euGVfFfDuUFsiDrMAG5uKfGYw5u/AqoweiQkQ==
dependencies:
"@sentry/core" "5.6.2"
"@sentry/types" "5.6.1"
"@sentry/utils" "5.6.1"
tslib "^1.9.3"
"@sentry/core@5.6.2":
version "5.6.2"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.6.2.tgz#8c5477654a83ebe41a72e86a79215deb5025e418"
integrity sha512-grbjvNmyxP5WSPR6UobN2q+Nss7Hvz+BClBT8QTr7VTEG5q89TwNddn6Ej3bGkaUVbct/GpVlI3XflWYDsnU6Q==
dependencies:
"@sentry/hub" "5.6.1"
"@sentry/minimal" "5.6.1"
"@sentry/types" "5.6.1"
"@sentry/utils" "5.6.1"
tslib "^1.9.3"
"@sentry/hub@5.6.1":
version "5.6.1"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.6.1.tgz#9f355c0abcc92327fbd10b9b939608aa4967bece"
integrity sha512-m+OhkIV5yTAL3R1+XfCwzUQka0UF/xG4py8sEfPXyYIcoOJ2ZTX+1kQJLy8QQJ4RzOBwZA+DzRKP0cgzPJ3+oQ==
dependencies:
"@sentry/types" "5.6.1"
"@sentry/utils" "5.6.1"
tslib "^1.9.3"
"@sentry/minimal@5.6.1":
version "5.6.1"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.6.1.tgz#09d92b26de0b24555cd50c3c33ba4c3e566009a1"
integrity sha512-ercCKuBWHog6aS6SsJRuKhJwNdJ2oRQVWT2UAx1zqvsbHT9mSa8ZRjdPHYOtqY3DoXKk/pLUFW/fkmAnpdMqRw==
dependencies:
"@sentry/hub" "5.6.1"
"@sentry/types" "5.6.1"
tslib "^1.9.3"
"@sentry/types@5.6.1":
version "5.6.1"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.6.1.tgz#5915e1ee4b7a678da3ac260c356b1cb91139a299"
integrity sha512-Kub8TETefHpdhvtnDj3kKfhCj0u/xn3Zi2zIC7PB11NJHvvPXENx97tciz4roJGp7cLRCJsFqCg4tHXniqDSnQ==
"@sentry/utils@5.6.1":
version "5.6.1"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.6.1.tgz#69d9e151e50415bc91f2428e3bcca8beb9bc2815"
integrity sha512-rfgha+UsHW816GqlSRPlniKqAZylOmQWML2JsujoUP03nPu80zdN43DK9Poy/d9OxBxv0gd5K2n+bFdM2kqLQQ==
dependencies:
"@sentry/types" "5.6.1"
tslib "^1.9.3"
"@sinonjs/commons@^1", "@sinonjs/commons@^1.0.2", "@sinonjs/commons@^1.4.0": "@sinonjs/commons@^1", "@sinonjs/commons@^1.0.2", "@sinonjs/commons@^1.4.0":
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.4.0.tgz#7b3ec2d96af481d7a0321252e7b1c94724ec5a78" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.4.0.tgz#7b3ec2d96af481d7a0321252e7b1c94724ec5a78"
...@@ -7091,11 +7143,6 @@ range-parser@^1.2.0, range-parser@~1.2.1: ...@@ -7091,11 +7143,6 @@ range-parser@^1.2.0, range-parser@~1.2.1:
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raven-js@^3.7.0:
version "3.27.2"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.27.2.tgz#6c33df952026cd73820aa999122b7b7737a66775"
integrity sha512-mFWQcXnhRFEQe5HeFroPaEghlnqy7F5E2J3Fsab189ondqUzcjwSVi7el7F36cr6PvQYXoZ1P2F5CSF2/azeMQ==
raw-body@2.4.0: raw-body@2.4.0:
version "2.4.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
...@@ -8493,7 +8540,7 @@ trim-right@^1.0.1: ...@@ -8493,7 +8540,7 @@ trim-right@^1.0.1:
dependencies: dependencies:
glob "^7.1.2" glob "^7.1.2"
tslib@^1.9.0: tslib@^1.9.0, tslib@^1.9.3:
version "1.10.0" version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment