diff --git a/.craft.yml b/.craft.yml index 79a363521fd5..2eaad6763309 100644 --- a/.craft.yml +++ b/.craft.yml @@ -9,7 +9,7 @@ targets: - name: github includeNames: /^sentry-.*$/ - name: gcs - includeNames: /^bundle\..*$/ + includeNames: /*\.js.*$/ bucket: sentry-js-sdk paths: - path: /{{version}}/ diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 000000000000..24255a79093e --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,132 @@ +# Upgrading from 4.x to 5.x + +In this version upgrade, there are a few breaking changes. This guide should help you update your code accordingly. + +## Integrations + +We moved optional integrations into their own package, called `@sentry/integrations`. Also, we made a few default +integrations now optional. This is probably the biggest breaking change regarding the upgrade. + +Integrations that are now opt-in and were default before: + +- Dedupe (responsible for sending the same error only once) +- ExtraErrorData (responsible for doing fancy magic, trying to extract data out of the error object using any + non-standard keys) + +Integrations that were pluggable/optional before, that also live in this package: + +- Angular (browser) +- Debug (browser/node) +- Ember (browser) +- ReportingObserver (browser) +- RewriteFrames (browser/node) +- Transaction (browser/node) +- Vue (browser) + +### How to use `@sentry/integrations`? + +Lets start with the approach if you install `@sentry/browser` / `@sentry/electron` with `npm` or `yarn`. + +Given you have a `Vue` application running, in order to use the `Vue` integration you need to do the following: + +With `4.x`: + +```js +import * as Sentry from '@sentry/browser'; + +Sentry.init({ + dsn: '___PUBLIC_DSN___', + integrations: [ + new Sentry.Integrations.Vue({ + Vue, + attachProps: true, + }), + ], +}); +``` + +With `5.x` you need to install `@sentry/integrations` and change the import. + +```js +import * as Sentry from '@sentry/browser'; +import * as Integrations from '@sentry/integrations'; + +Sentry.init({ + dsn: '___PUBLIC_DSN___', + integrations: [ + new Integrations.Vue({ + Vue, + attachProps: true, + }), + ], +}); +``` + +In case you are using the CDN version or the Loader, we provide a standalone file for every integration, you can use it +like this: + +```html + + + + + + + + +``` + +## New Scope functions + +We realized how annoying it is to set a whole object using `setExtra`, that's why there are now a few new methods on the +`Scope`. + +```typescript +setTags(tags: { [key: string]: string }): this; +setExtras(extras: { [key: string]: any }): this; +clearBreadcrumbs(): this; +``` + +So you can do this now: + +```js +// New in 5.x setExtras +Sentry.withScope(scope => { + scope.setExtras(errorInfo); + Sentry.captureException(error); +}); + +// vs. 4.x +Sentry.withScope(scope => { + Object.keys(errorInfo).forEach(key => { + scope.setExtra(key, errorInfo[key]); + }); + Sentry.captureException(error); +}); +``` + +## Less Async API + +We removed a lot of the internal async code since in certain situations it generated a lot of memory pressure. This +really only affects you if you where either using the `BrowserClient` or `NodeClient` directly. + +So all the `capture*` functions now instead of returning `Promise` return `string | undefined`. `string` in +this case is the `event_id`, in case the event will not be sent because of filtering it will return `undefined`. + +## `close` vs. `flush` + +In `4.x` we had both `close` and `flush` on the `Client` draining the internal queue of events, helpful when you were +using `@sentry/node` on a serverless infrastructure. + +Now `close` and `flush` work similar, with the difference that if you call `close` in addition to returing a `Promise` +that you can await it also **disables** the client so it will not send any future events. diff --git a/Makefile b/Makefile index ca6552f8b761..2c3f5e968fe7 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,3 @@ -bump: - yarn lerna version --exact --no-git-tag-version --no-push -.PHONY: bump - prepare-release: yarn clean yarn build @@ -9,22 +5,6 @@ prepare-release: yarn test .PHONY: prepare-release -publish-npm: - cd packages/browser; npm publish - cd packages/core; npm publish - cd packages/hub; npm publish - cd packages/integrations; npm publish - cd packages/minimal; npm publish - cd packages/node; npm publish - # cd packages/types; npm publish - # cd packages/typescript; npm publish - cd packages/utils; npm publish -.PHONY: publish-npm - -publish-cdn: - node scripts/browser-upload-cdn.js -.PHONY: publish-cdn - build-docs: rm -rf ./docs yarn typedoc --options ./typedoc.js @@ -39,6 +19,3 @@ publish-docs: build-docs git push origin gh-pages git checkout master .PHONY: publish-docs - -release: bump prepare-release publish-npm publish-cdn -.PHONY: release diff --git a/packages/browser/examples/app.js b/packages/browser/examples/app.js index a01f093ea725..af5e4b6cae6b 100644 --- a/packages/browser/examples/app.js +++ b/packages/browser/examples/app.js @@ -5,8 +5,8 @@ class HappyIntegration { } setupOnce() { - Sentry.addGlobalEventProcessor(async event => { - const self = getCurrentHub().getIntegration(HappyIntegration); + Sentry.addGlobalEventProcessor(event => { + const self = Sentry.getCurrentHub().getIntegration(HappyIntegration); // Run the integration ONLY when it was installed on the current Hub if (self) { if (event.message === 'Happy Message') { @@ -19,7 +19,7 @@ class HappyIntegration { } class HappyTransport extends Sentry.Transports.BaseTransport { - captureEvent(event) { + sendEvent(event) { console.log( `This is the place where you'd implement your own sending logic. It'd get url: ${this.url} and an event itself:`, event, @@ -36,11 +36,11 @@ Sentry.init({ dsn: 'https://363a337c11a64611be4845ad6e24f3ac@sentry.io/297378', // An array of strings or regexps that'll be used to ignore specific errors based on their type/message ignoreErrors: [/PickleRick_\d\d/, 'RangeError'], - // // An array of strings or regexps that'll be used to ignore specific errors based on their origin url + // An array of strings or regexps that'll be used to ignore specific errors based on their origin url blacklistUrls: ['external-lib.js'], - // // An array of strings or regexps that'll be used to allow specific errors based on their origin url + // An array of strings or regexps that'll be used to allow specific errors based on their origin url whitelistUrls: ['http://localhost:5000', 'https://browser.sentry-cdn'], - // // Debug mode with valuable initialization/lifecycle informations. + // Debug mode with valuable initialization/lifecycle informations. debug: true, // Whether SDK should be enabled or not. enabled: true, diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 5a8269b90e77..9c7f44c6cd8a 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -33,11 +33,22 @@ export { defaultIntegrations, forceLoad, init, lastEventId, onLoad, showReportDi export { SDK_NAME, SDK_VERSION } from './version'; import { Integrations as CoreIntegrations } from '@sentry/core'; +import { getGlobalObject } from '@sentry/utils/misc'; import * as BrowserIntegrations from './integrations'; import * as Transports from './transports'; +let windowIntegrations = {}; + +// tslint:disable: no-unsafe-any +const _window = getGlobalObject() as any; +if (_window.Sentry && _window.Sentry.Integrations) { + windowIntegrations = _window.Sentry.Integrations; +} +// tslint:enable: no-unsafe-any + const INTEGRATIONS = { + ...windowIntegrations, ...CoreIntegrations, ...BrowserIntegrations, }; diff --git a/packages/browser/src/loader.js b/packages/browser/src/loader.js index a91c5288d203..aea7e90ba115 100644 --- a/packages/browser/src/loader.js +++ b/packages/browser/src/loader.js @@ -150,26 +150,27 @@ } } - // We don't want to _window.Sentry = _window.Sentry || { ... } since we want to make sure - // that the first Sentry "instance" is our with onLoad - _window[_namespace] = { - onLoad: function (callback) { - onLoadCallbacks.push(callback); - if (lazy && !forceLoad) { - return; - } - injectSdk(onLoadCallbacks); - }, - forceLoad: function() { - forceLoad = true; - if (lazy) { - setTimeout(function() { - injectSdk(onLoadCallbacks); - }); - } + // We make sure we do not overwrite window.Sentry since there could be already integrations in there + _window[_namespace] = _window[_namespace] || {}; + + _window[_namespace].onLoad = function (callback) { + onLoadCallbacks.push(callback); + if (lazy && !forceLoad) { + return; } + injectSdk(onLoadCallbacks); }; + _window[_namespace].forceLoad = function() { + forceLoad = true; + if (lazy) { + setTimeout(function() { + injectSdk(onLoadCallbacks); + }); + } + }; + + [ 'init', 'addBreadcrumb', diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index f2981a8d1a2f..ed74871e184f 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -1,5 +1,5 @@ import { Scope } from '@sentry/hub'; -import { Client, Event, EventHint, Integration, IntegrationClass, Options, Severity } from '@sentry/types'; +import { Client, Event, EventHint, Integration, IntegrationClass, Options, SdkInfo, Severity } from '@sentry/types'; import { isPrimitive, isThenable } from '@sentry/utils/is'; import { logger } from '@sentry/utils/logger'; import { uuid4 } from '@sentry/utils/misc'; @@ -278,6 +278,8 @@ export abstract class BaseClient implement prepared.event_id = uuid4(); } + this._addIntegrations(prepared.sdk); + // We prepare the result here with a resolved Event. let result = SyncPromise.resolve(prepared); @@ -291,6 +293,17 @@ export abstract class BaseClient implement return result; } + /** + * This function adds all used integrations to the SDK info in the event. + * @param sdkInfo The sdkInfo of the event that will be filled with all integrations. + */ + protected _addIntegrations(sdkInfo?: SdkInfo): void { + const integrationsArray = Object.keys(this._integrations); + if (sdkInfo && integrationsArray.length > 0) { + sdkInfo.integrations = integrationsArray; + } + } + /** * Processes an event (either error or message) and sends it to Sentry. * diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 3c03718f9464..c2fe2a02be19 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -21,19 +21,19 @@ export function getIntegrationsToSetup(options: Options): Integration[] { // Leave only unique default integrations, that were not overridden with provided user integrations defaultIntegrations.forEach(defaultIntegration => { if ( - userIntegrationsNames.indexOf(getIntegrationName(defaultIntegration)) === -1 && - pickedIntegrationsNames.indexOf(getIntegrationName(defaultIntegration)) === -1 + userIntegrationsNames.indexOf(defaultIntegration.name) === -1 && + pickedIntegrationsNames.indexOf(defaultIntegration.name) === -1 ) { integrations.push(defaultIntegration); - pickedIntegrationsNames.push(getIntegrationName(defaultIntegration)); + pickedIntegrationsNames.push(defaultIntegration.name); } }); // Don't add same user integration twice userIntegrations.forEach(userIntegration => { - if (pickedIntegrationsNames.indexOf(getIntegrationName(userIntegration)) === -1) { + if (pickedIntegrationsNames.indexOf(userIntegration.name) === -1) { integrations.push(userIntegration); - pickedIntegrationsNames.push(getIntegrationName(userIntegration)); + pickedIntegrationsNames.push(userIntegration.name); } }); } else if (typeof userIntegrations === 'function') { @@ -48,12 +48,12 @@ export function getIntegrationsToSetup(options: Options): Integration[] { /** Setup given integration */ export function setupIntegration(integration: Integration): void { - if (installedIntegrations.indexOf(getIntegrationName(integration)) !== -1) { + if (installedIntegrations.indexOf(integration.name) !== -1) { return; } integration.setupOnce(addGlobalEventProcessor, getCurrentHub); - installedIntegrations.push(getIntegrationName(integration)); - logger.log(`Integration installed: ${getIntegrationName(integration)}`); + installedIntegrations.push(integration.name); + logger.log(`Integration installed: ${integration.name}`); } /** @@ -65,20 +65,8 @@ export function setupIntegration(integration: Integration): void { export function setupIntegrations(options: O): IntegrationIndex { const integrations: IntegrationIndex = {}; getIntegrationsToSetup(options).forEach(integration => { - integrations[getIntegrationName(integration)] = integration; + integrations[integration.name] = integration; setupIntegration(integration); }); return integrations; } - -/** - * Returns the integration static id. - * @param integration Integration to retrieve id - */ -function getIntegrationName(integration: Integration): string { - /** - * @depracted - */ - // tslint:disable-next-line:no-unsafe-any - return (integration as any).constructor.id || integration.name; -} diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 904006ed4be4..e4e503a943b4 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -12,6 +12,10 @@ "publishConfig": { "access": "public" }, + "main": "dist/index.js", + "module": "esm/index.js", + "browser": "dist/index.js", + "types": "dist/index.d.ts", "dependencies": { "@sentry/types": "5.0.0-rc.3", "@sentry/utils": "5.0.0-rc.3" @@ -33,7 +37,7 @@ "typescript": "^3.3.3333" }, "scripts": { - "build": "run-p build:es5 build:bundle", + "build": "run-p build:es5 build:esm build:bundle", "build:es5": "tsc -p tsconfig.build.json", "build:esm": "tsc -p tsconfig.esm.json", "build:watch": "run-p build:watch:es5 build:watch:esm", @@ -41,6 +45,7 @@ "build:watch:esm": "tsc -p tsconfig.esm.json -w --preserveWatchOutput", "build:bundle": "rollup --config", "clean": "rimraf dist coverage *.js *.js.map *.d.ts", + "link:yarn": "yarn link", "lint": "run-s lint:prettier lint:tslint", "lint:prettier": "prettier-check \"{src,test}/**/*.ts\"", "lint:tslint": "tslint -t stylish -p .", diff --git a/packages/integrations/rollup.config.js b/packages/integrations/rollup.config.js index 11df82a481b5..e431740e6904 100644 --- a/packages/integrations/rollup.config.js +++ b/packages/integrations/rollup.config.js @@ -43,49 +43,47 @@ const plugins = [ commonjs(), ]; -function toPascalCase(string) { - return `${string}` - .replace(new RegExp(/[-_]+/, 'g'), ' ') - .replace(new RegExp(/[^\w\s]/, 'g'), '') - .replace(new RegExp(/\s+(.)(\w+)/, 'g'), ($1, $2, $3) => `${$2.toUpperCase() + $3.toLowerCase()}`) - .replace(new RegExp(/\s/, 'g'), '') - .replace(new RegExp(/\w/), s => s.toUpperCase()); -} - -function mergeIntoSentry(name) { +function mergeIntoSentry() { return ` - if (window.Sentry && window.Sentry.Integrations) { - window.Sentry.Integrations['${name}'] = exports.${name}; - } else { - if ((typeof __SENTRY_INTEGRATIONS_LOG === 'undefined')) { - console.warn('Sentry.Integrations is not defined, make sure you included this script after the SDK.'); - console.warn('In case you were using the loader, we added the Integration is now available under SentryIntegrations.${name}'); - console.warn('To disable these warning set __SENTRY_INTEGRATIONS_LOG = true; somewhere before loading this script.'); - } - window.SentryIntegrations = window.SentryIntegrations || {}; - window.SentryIntegrations['${name}'] = exports.${name}; - } + __window.Sentry = __window.Sentry || {}; + __window.Sentry.Integrations = __window.Sentry.Integrations || {}; + Object.assign(__window.Sentry.Integrations, exports); `; } function allIntegrations() { - return fs.readdirSync('./src').filter(file => file != 'modules.ts'); + return fs.readdirSync('./src').filter(file => file != 'index.ts'); } function loadAllIntegrations() { - return allIntegrations().map(file => ({ - input: `src/${file}`, - output: { - banner: '(function (window) {', - intro: 'var exports = {};', - footer: '}(window));', - outro: mergeIntoSentry(toPascalCase(file.replace('.ts', ''))), - file: `build/${file.replace('.ts', '.js')}`, - format: 'cjs', - sourcemap: true, + const builds = []; + [ + { + extension: '.js', + plugins, }, - plugins, - })); + { + extension: '.min.js', + plugins: [...plugins, terserInstance], + }, + ].forEach(build => { + builds.push( + ...allIntegrations().map(file => ({ + input: `src/${file}`, + output: { + banner: '(function (__window) {', + intro: 'var exports = {};', + outro: mergeIntoSentry(), + footer: '}(window));', + file: `build/${file.replace('.ts', build.extension)}`, + format: 'cjs', + sourcemap: true, + }, + plugins: build.plugins, + })), + ); + }); + return builds; } export default loadAllIntegrations(); diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts new file mode 100644 index 000000000000..f9410442d8e4 --- /dev/null +++ b/packages/integrations/src/index.ts @@ -0,0 +1,8 @@ +export { Angular } from './angular'; +export { Debug } from './debug'; +export { Ember } from './ember'; +export { ExtraErrorData } from './extraerrordata'; +export { ReportingObserver } from './reportingobserver'; +export { RewriteFrames } from './rewriteframes'; +export { Transaction } from './transaction'; +export { Vue } from './vue'; diff --git a/packages/node/src/integrations/index.ts b/packages/node/src/integrations/index.ts index 18f64fd5b998..772c0e8f3f86 100644 --- a/packages/node/src/integrations/index.ts +++ b/packages/node/src/integrations/index.ts @@ -3,3 +3,4 @@ export { Http } from './http'; export { OnUncaughtException } from './onuncaughtexception'; export { OnUnhandledRejection } from './onunhandledrejection'; export { LinkedErrors } from './linkederrors'; +export { Modules } from './modules'; diff --git a/packages/integrations/src/modules.ts b/packages/node/src/integrations/modules.ts similarity index 100% rename from packages/integrations/src/modules.ts rename to packages/node/src/integrations/modules.ts diff --git a/scripts/browser-upload-cdn.js b/scripts/browser-upload-cdn.js deleted file mode 100755 index 2e25ecca4784..000000000000 --- a/scripts/browser-upload-cdn.js +++ /dev/null @@ -1,94 +0,0 @@ -'use strict'; - -const Storage = require('@google-cloud/storage'); -const path = require('path'); -const os = require('os'); -const fs = require('fs'); - -const bundleFilesRegex = /^bundle.*\.js.*$/; -const rootDir = path.dirname(__dirname); -const browserDir = path.join(rootDir, 'packages', 'browser'); -const browserBuildDir = path.join(browserDir, 'build'); - -/** Return full paths of files to upload */ -function findFiles() { - const bundleFiles = fs - .readdirSync(browserBuildDir) - .filter(filename => filename.match(bundleFilesRegex)) - .map(filename => path.join(browserBuildDir, filename)); - return bundleFiles; -} - -/** Upload sentry-browser bundles to a GCS bucket */ -async function uploadFiles() { - const gcsConfigPath = - process.env.BROWSER_GOOGLE_APPLICATION_CREDENTIALS || - path.join(os.homedir(), '.gcs', 'sentry-browser-sdk.json'); - console.log(`Reading GCS configuration from "${gcsConfigPath}"...`); - - const gcsConfig = fs.existsSync(gcsConfigPath) - ? JSON.parse(fs.readFileSync(gcsConfigPath)) - : undefined; - - if (!gcsConfig) { - console.error( - 'Google Storage configuration (service account key) not found.\n' + - `Place it at ${gcsConfigPath} or use the environment variable ` + - '(BROWSER_GOOGLE_APPLICATION_CREDENTIALS) to specify the path.', - ); - process.exit(1); - } - - const projectId = - process.env.BROWSER_GOOGLE_PROJECT_ID || gcsConfig.project_id; - if (!projectId) { - console.error('Google project ID not found.'); - process.exit(1); - } - - const bucketName = - process.env.BROWSER_GOOGLE_BUCKET_NAME || gcsConfig.bucket_name; - if (!bucketName) { - console.error('Bucket name not found in the configuration.'); - process.exit(1); - } - - const bundleFiles = findFiles(); - if (!bundleFiles.length) { - console.error('Error: no files to upload!'); - process.exit(1); - } - - const browserPackageJson = path.join(browserDir, 'package.json'); - const version = - JSON.parse(fs.readFileSync(browserPackageJson)).version || 'unreleased'; - - const storage = new Storage({ - projectId, - credentials: gcsConfig, - }); - - const bucket = storage.bucket(bucketName); - const cacheAge = 31536000; // 1 year - - await Promise.all( - bundleFiles.map(async filepath => { - const destination = path.join(version, path.basename(filepath)); - const options = { - gzip: true, - destination: destination, - metadata: { - cacheControl: `public, max-age=${cacheAge}`, - }, - }; - await bucket.upload(filepath, options); - console.log(`Uploaded "${destination}"`); - }), - ); - console.log('Upload complete.'); -} - -uploadFiles().catch(error => { - console.error('Error occurred:', error); - process.exit(1); -}); diff --git a/scripts/pack-and-upload.sh b/scripts/pack-and-upload.sh index 734ad1d46f62..6b3f1b0ff54e 100755 --- a/scripts/pack-and-upload.sh +++ b/scripts/pack-and-upload.sh @@ -15,6 +15,8 @@ node scripts/package-and-upload-to-zeus.js # Upload "sentry-browser" bundles zeus upload -t "application/javascript" ./packages/browser/build/bundle* +# Upload "integrations" bundles +zeus upload -t "application/javascript" ./packages/integrations/build/* # Upload docs make build-docs diff --git a/scripts/test.sh b/scripts/test.sh index cbb55d0ff569..ea976b67fd9e 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -4,8 +4,8 @@ set -e # We need this check to skip engines check for typescript-tslint-plugin package if [[ "$(cut -d. -f1 <<< "$TRAVIS_NODE_VERSION")" -le 6 ]]; then yarn install --ignore-engines - yarn build --ignore="@sentry/browser" - yarn test --ignore="@sentry/browser" # latest version of karma doesn't run on node 6 + yarn build --ignore="@sentry/browser" --ignore="@sentry/integrations" + yarn test --ignore="@sentry/browser" --ignore="@sentry/integrations" # latest version of karma doesn't run on node 6 else yarn install yarn build