Repository: airbrake/airbrake-js Branch: master Commit: 792552803c40 Files: 125 Total size: 188.1 KB Directory structure: gitextract_8l49k84a/ ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── dependabot.yml │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .prettierrc.toml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── lerna.json ├── package.json ├── packages/ │ ├── browser/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── babel.config.js │ │ ├── examples/ │ │ │ ├── angular/ │ │ │ │ └── README.md │ │ │ ├── angularjs/ │ │ │ │ └── README.md │ │ │ ├── extjs/ │ │ │ │ └── README.md │ │ │ ├── legacy/ │ │ │ │ ├── README.md │ │ │ │ ├── app.js │ │ │ │ └── index.html │ │ │ ├── nextjs/ │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── components/ │ │ │ │ │ ├── date.js │ │ │ │ │ ├── error_boundary.js │ │ │ │ │ ├── layout.js │ │ │ │ │ └── layout.module.css │ │ │ │ ├── lib/ │ │ │ │ │ └── posts.js │ │ │ │ ├── package.json │ │ │ │ ├── pages/ │ │ │ │ │ ├── _app.js │ │ │ │ │ ├── _error.js │ │ │ │ │ ├── api/ │ │ │ │ │ │ └── hello.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── posts/ │ │ │ │ │ └── [id].js │ │ │ │ ├── posts/ │ │ │ │ │ ├── pre-rendering.md │ │ │ │ │ └── ssg-ssr.md │ │ │ │ └── styles/ │ │ │ │ ├── global.css │ │ │ │ └── utils.module.css │ │ │ ├── rails/ │ │ │ │ └── README.md │ │ │ ├── react/ │ │ │ │ └── README.md │ │ │ ├── redux/ │ │ │ │ └── README.md │ │ │ ├── svelte/ │ │ │ │ └── README.md │ │ │ └── vuejs/ │ │ │ └── README.md │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── rollup.config.js │ │ ├── src/ │ │ │ ├── base_notifier.ts │ │ │ ├── filter/ │ │ │ │ ├── angular_message.ts │ │ │ │ ├── debounce.ts │ │ │ │ ├── filter.ts │ │ │ │ ├── ignore_noise.ts │ │ │ │ ├── performance_filter.ts │ │ │ │ ├── uncaught_message.ts │ │ │ │ └── window.ts │ │ │ ├── func_wrapper.ts │ │ │ ├── http_req/ │ │ │ │ ├── api.ts │ │ │ │ ├── fetch.ts │ │ │ │ ├── index.ts │ │ │ │ └── node.ts │ │ │ ├── index.ts │ │ │ ├── instrumentation/ │ │ │ │ ├── console.ts │ │ │ │ ├── dom.ts │ │ │ │ ├── fetch.ts │ │ │ │ ├── location.ts │ │ │ │ ├── unhandledrejection.ts │ │ │ │ └── xhr.ts │ │ │ ├── jsonify_notice.ts │ │ │ ├── metrics.ts │ │ │ ├── notice.ts │ │ │ ├── notifier.ts │ │ │ ├── options.ts │ │ │ ├── processor/ │ │ │ │ ├── esp.ts │ │ │ │ └── processor.ts │ │ │ ├── queries.ts │ │ │ ├── queues.ts │ │ │ ├── remote_settings.ts │ │ │ ├── routes.ts │ │ │ ├── scope.ts │ │ │ ├── tdshared.ts │ │ │ └── version.ts │ │ ├── tests/ │ │ │ ├── client.test.js │ │ │ ├── historian.test.js │ │ │ ├── jsonify_notice.test.js │ │ │ ├── processor/ │ │ │ │ └── stacktracejs.test.js │ │ │ ├── remote_settings.test.js │ │ │ └── truncate.test.js │ │ ├── tsconfig.cjs.json │ │ ├── tsconfig.esm.json │ │ ├── tsconfig.json │ │ ├── tsconfig.umd.json │ │ └── tslint.json │ └── node/ │ ├── LICENSE │ ├── README.md │ ├── babel.config.js │ ├── examples/ │ │ ├── express/ │ │ │ ├── README.md │ │ │ ├── app.js │ │ │ └── package.json │ │ └── nodejs/ │ │ ├── README.md │ │ ├── app.js │ │ └── package.json │ ├── jest.config.js │ ├── package.json │ ├── src/ │ │ ├── filter/ │ │ │ └── node.ts │ │ ├── index.ts │ │ ├── instrumentation/ │ │ │ ├── debug.ts │ │ │ ├── express.ts │ │ │ ├── http.ts │ │ │ ├── https.ts │ │ │ ├── mysql.ts │ │ │ ├── mysql2.ts │ │ │ ├── pg.ts │ │ │ └── redis.ts │ │ ├── notifier.ts │ │ ├── scope.ts │ │ └── version.ts │ ├── tests/ │ │ ├── notifier.test.js │ │ └── routes.test.js │ ├── tsconfig.cjs.json │ ├── tsconfig.esm.json │ ├── tsconfig.json │ └── tslint.json ├── tsconfig.esm.json ├── tsconfig.json └── tslint.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ [**.ts] indent_size = 2 ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve Airbrake title: '' labels: bug assignees: '' --- # 🐞 bug report ### Affected Package The issue is caused by package @airbrake/.... ### Is this a regression? Yes, the previous version in which this bug was not present was: .... ### Description A clear and concise description of the problem... ## 🔬 Minimal Reproduction https://gist.github.com/... ## 🔥 Exception or Error




## 🌍 Your Environment **@airbrake/\* version:**




**Anything else relevant?** ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest a feature for Airbrake title: '' labels: enhancement assignees: '' --- # 🚀 feature request ### Relevant Package This feature request is for @airbrake/.... ### Description A clear and concise description of the problem or missing capability... ### Describe the solution you'd like If you have a solution in mind, please describe it. ### Describe alternatives you've considered Have you considered any alternative solutions or workarounds? ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: npm directory: "/" schedule: interval: daily time: "19:00" timezone: US/Central ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: [push] permissions: contents: read jobs: lint_and_test: runs-on: ubuntu-latest strategy: matrix: node-version: [14.x, 16.x, 18.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: yarn install - run: yarn lint - run: yarn build - run: yarn test ================================================ FILE: .gitignore ================================================ node_modules .rpt2_cache packages/*/yarn.lock packages/*/package-lock.json packages/*/dist packages/*/esm packages/browser/umd .ackrc ================================================ FILE: .prettierrc.toml ================================================ singleQuote = true trailingComma = "es5" arrowParens = "always" ================================================ FILE: CHANGELOG.md ================================================ # Airbrake JS Changelog ### master ### [2.1.9] (March 6, 2025) #### browser - Added the `keysAllowlist` option, which is a counter-part to the `keysBlocklist` option. It filters out all the data from the notice except the specified keys ([#1335](https://github.com/airbrake/airbrake-js/pull/1335)) - Added support for error reporting of "falsey" errors such as `null`, `NaN`, `undefined`, `false`, `""` ([#1345](https://github.com/airbrake/airbrake-js/pull/1345)) - Added the `instrumentation.unhandledrejection` option, which enables/disables the Airbrake handler for the `unhandledrejection` event ([#1356](https://github.com/airbrake/airbrake-js/pull/1356)) ### [2.1.8] (December 6, 2022) #### browser - Fixed relative import issues with Yarn's Plug'n'Play feature ([#1135](https://github.com/airbrake/airbrake-js/pull/1135)) - Stop filtering the `context` field in the notice payload. This payload contains service information and it should never be modified ([#1325](https://github.com/airbrake/airbrake-js/pull/1325)) - Bumped `cross-fetch` dependency to `^3.1.5` (fixes a Dependabot security alert) ([#1322](https://github.com/airbrake/airbrake-js/issues/1322)) ### [2.1.7] (October 4, 2021) - [browser/node] Fixed incorrect `yarn.lock` references ([#1132](https://github.com/airbrake/airbrake-js/pull/1132)) ### [2.1.6] (October 4, 2021) - [browser] Fixed not being able to attach a response type when sending a performance breakdown ([#1128](https://github.com/airbrake/airbrake-js/pull/1128)) ### [2.1.5] (June 2, 2021) - [node] Specify which versions of node are supported ([#1038](https://github.com/airbrake/airbrake-js/pull/1038)) ### [2.1.4] (April 16, 2021) - [browser] Fixed `TypeError: undefined is not an object (evaluating 'e.searchParams.append')` occurring in old browsers that don't support `Object.entries` (such as Internet Explorer) ([#1001](https://github.com/airbrake/airbrake-js/pull/1001), [#1002](https://github.com/airbrake/airbrake-js/pull/1002)) ### [2.1.3] (February 22, 2021) - [browser/node] Fixed missing library files in v2.1.2 ### [2.1.2] (February 22, 2021) - [browser] Started catching errors in promises that occur in `RemoteSettings` ([#949](https://github.com/airbrake/airbrake-js/pull/949)) ### [2.1.1] (February 20, 2021) - [browser] Removed unwanted `debugger` statement in `base_notifier.js` in the distribution package ([#948](https://github.com/airbrake/airbrake-js/pull/948)) ### [2.1.0] (February 19, 2021) - [browser/node] Added the `queryStats` and the `queueStats` option. They allow/forbid reporting of queries or queues, respectively ([#945](https://github.com/airbrake/airbrake-js/pull/945)) - [browser/node] Fixed `_ignoreNextWindowError` undefined error when wrapping errors ([#944](https://github.com/airbrake/airbrake-js/pull/944)) - [node] Fixed warnings on loading of `notifier.js` when using Webpack ([#936](https://github.com/airbrake/airbrake-js/pull/936)) ### [2.0.0] (February 18, 2021) - [browser/node] Removed deprecated `ignoreWindowError` option ([#929](https://github.com/airbrake/airbrake-js/pull/929)) - [browser/node] Removed deprecated `keysBlacklist` option ([#930](https://github.com/airbrake/airbrake-js/pull/930)) - [browser/node] Introduced the `remoteConfigHost` option. This option configures the host that the notifier fetch remote configuration from. ([#940](https://github.com/airbrake/airbrake-js/pull/940)) - [browser/node] Introduced the `apmHost` option. This option configures the host that the notifier should send APM events to. ([#940](https://github.com/airbrake/airbrake-js/pull/940)) - [browser/node] Introduced the `errorNotifications` option. This options configures ability to send errors ([#940](https://github.com/airbrake/airbrake-js/pull/940)) - [browser/node] Introduced the `remoteConfig` option. This option configures the remote configuration feature ([#940](https://github.com/airbrake/airbrake-js/pull/940)) - [browser/node] Added support for the remote configuration feature ([#940](https://github.com/airbrake/airbrake-js/pull/940)) ### [1.4.2] (December 22, 2020) #### Changed - [node] Conditionally initialize ScopeManager ([#894](https://github.com/airbrake/airbrake-js/pull/894)) - [browser] Add the ability to disable console tracking via instrumentation ([#860](https://github.com/airbrake/airbrake-js/pull/860)) ### [1.4.1] (August 10, 2020) #### Changed - [browser] Unhandled rejection errors now include `unhandledRejection: true` as part of their `context` ([#795](https://github.com/airbrake/airbrake-js/pull/795)) ### [1.4.0] (July 22, 2020) #### Changed - [browser/node] `notify` now includes the `url` property on the returned `INotice` object ([#780](https://github.com/airbrake/airbrake-js/pull/780)) ### [1.3.0] (June 19, 2020) #### Changed - [browser/node] Deprecate `keysBlacklist` in favor of `keysBlocklist` ### [1.2.0] (May 29, 2020) #### Added - [node] New method to filter performance metrics ([#726](https://github.com/airbrake/airbrake-js/pull/726)) ### [1.1.3] (May 26, 2020) #### Changed - [browser/node] Remove onUnhandledrejection parameter type ### [1.1.2] (May 5, 2020) #### Fixed - [browser] Add guard for window being undefined ([#684](https://github.com/airbrake/airbrake-js/pull/684)) - [node] Report URL using `req.originalUrl` instead of `req.path` in Express apps ([#691](https://github.com/airbrake/airbrake-js/pull/691)) ### [1.1.1] (April 28, 2020) #### Fixed - [node] Express route stat reporting ([#671](https://github.com/airbrake/airbrake-js/pull/671)) ### [1.1.0] (April 22, 2020) #### Changed - [browser/node] Build process updates. Bumping minor version for this. See [#646](https://github.com/airbrake/airbrake-js/pull/646) - [browser/node] Documentation updates ### [1.0.7] (April 8, 2020) #### Added - [node] New config option to disable performance stats #### Changed - [browser/node] Build config updates - [browser/node] Update dependencies - [browser/node] Documentation updates - [browser/node] Update linting config #### Fixed - [browser] Fix stacktrace test for node v10 - [browser/node] Fix linting errors ### [1.0.6] (November 18, 2019) ### [1.0.4] (November 12, 2019) ### [1.0.3] (November 7, 2019) ### [1.0.2] (October 28, 2019) ### [1.0.1] (October 28, 2019) ### [1.0.0] (October 21, 2019) [1.0.0]: https://github.com/airbrake/airbrake-js/releases/tag/v1.0.0 [1.0.1]: https://github.com/airbrake/airbrake-js/releases/tag/v1.0.1 [1.0.2]: https://github.com/airbrake/airbrake-js/releases/tag/v1.0.2 [1.0.3]: https://github.com/airbrake/airbrake-js/releases/tag/v1.0.3 [1.0.4]: https://github.com/airbrake/airbrake-js/releases/tag/v1.0.4 [1.0.6]: https://github.com/airbrake/airbrake-js/releases/tag/v1.0.6 [1.0.7]: https://github.com/airbrake/airbrake-js/releases/tag/v1.0.7 [1.1.0]: https://github.com/airbrake/airbrake-js/releases/tag/v1.1.0 [1.1.1]: https://github.com/airbrake/airbrake-js/releases/tag/v1.1.1 [1.1.2]: https://github.com/airbrake/airbrake-js/releases/tag/v1.1.2 [1.1.3]: https://github.com/airbrake/airbrake-js/releases/tag/v1.1.3 [1.2.0]: https://github.com/airbrake/airbrake-js/releases/tag/v1.2.0 [1.3.0]: https://github.com/airbrake/airbrake-js/releases/tag/v1.3.0 [1.4.0]: https://github.com/airbrake/airbrake-js/releases/tag/v1.4.0 [1.4.1]: https://github.com/airbrake/airbrake-js/releases/tag/v1.4.1 [1.4.2]: https://github.com/airbrake/airbrake-js/releases/tag/v1.4.2 [2.0.0]: https://github.com/airbrake/airbrake-js/releases/tag/v2.0.0 [2.1.0]: https://github.com/airbrake/airbrake-js/releases/tag/v2.1.0 [2.1.1]: https://github.com/airbrake/airbrake-js/releases/tag/v2.1.1 [2.1.2]: https://github.com/airbrake/airbrake-js/releases/tag/v2.1.2 [2.1.3]: https://github.com/airbrake/airbrake-js/releases/tag/v2.1.3 [2.1.4]: https://github.com/airbrake/airbrake-js/releases/tag/v2.1.4 [2.1.5]: https://github.com/airbrake/airbrake-js/releases/tag/v2.1.5 [2.1.6]: https://github.com/airbrake/airbrake-js/releases/tag/v2.1.6 [2.1.7]: https://github.com/airbrake/airbrake-js/releases/tag/v2.1.7 [2.1.8]: https://github.com/airbrake/airbrake-js/releases/tag/v2.1.8 ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Bug fixes and improvements may be submitted in the form of pull requests. ## Development Setup You will need [Node.js](https://nodejs.org/download) version 10+ and [yarn](https://yarnpkg.com/en/docs/install). `airbrake-js` is a monorepo containing multiple packages. [Lerna](https://lerna.js.org/) and [yarn workspaces](https://yarnpkg.com/features/workspaces) are used to manage them. To get started, you'll need to install the project dependencies and run the build script: ```sh yarn yarn build ``` ## Building Run `yarn build` within a package directory to build that specific package, or run it at the project root to build all packages at once. `yarn build` must be run before testing or linting. ## Testing Run `yarn test` within a package directory to run tests for that specific package, or run it at the project root to run tests for all packages at once. ## Linting Run `yarn lint` within a package directory to lint that specific package, or run it at the project root to lint all packages at once. ================================================ FILE: LICENSE.md ================================================ # MIT License Copyright © 2022 Airbrake Technologies, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

# Official Airbrake Notifiers for JavaScript [![Build Status](https://github.com/airbrake/airbrake-js/workflows/CI/badge.svg?branch=master)](https://github.com/airbrake/airbrake-js/actions?query=branch%3Amaster) [![npm version](https://img.shields.io/npm/v/@airbrake/browser.svg)](https://www.npmjs.com/package/@airbrake/browser) Please choose one of the following packages: [@airbrake/browser](packages/browser) for web browsers. [@airbrake/node](packages/node) for Node.js. ================================================ FILE: lerna.json ================================================ { "version": "2.1.9", "npmClient": "yarn", "useWorkspaces": true } ================================================ FILE: package.json ================================================ { "name": "airbrake", "private": true, "devDependencies": { "concurrently": "^7.6.0", "lerna": "^6.0.3", "prettier": "^2.0.2", "tslint": "^6.1.0", "tslint-config-prettier": "^1.18.0", "tslint-plugin-prettier": "^2.3.0", "typescript": "^4.0.2" }, "scripts": { "build": "lerna run build", "build:watch": "lerna run build:watch --parallel", "clean": "lerna run clean", "lint": "lerna run lint", "test": "lerna run test" }, "workspaces": [ "packages/*" ] } ================================================ FILE: packages/browser/LICENSE ================================================ MIT License Copyright (c) 2020 Airbrake Technologies, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/browser/README.md ================================================

# Official Airbrake Notifier for Browsers [![Build Status](https://github.com/airbrake/airbrake-js/workflows/CI/badge.svg?branch=master)](https://github.com/airbrake/airbrake-js/actions?query=branch%3Amaster) [![npm version](https://img.shields.io/npm/v/@airbrake/browser.svg)](https://www.npmjs.com/package/@airbrake/browser) [![npm dm](https://img.shields.io/npm/dm/@airbrake/browser.svg)](https://www.npmjs.com/package/@airbrake/browser) [![npm dt](https://img.shields.io/npm/dt/@airbrake/browser.svg)](https://www.npmjs.com/package/@airbrake/browser) The official Airbrake notifier for capturing JavaScript errors in web browsers and reporting them to [Airbrake](http://airbrake.io). If you're looking for Node.js support, there is a [separate package](https://github.com/airbrake/airbrake-js/tree/master/packages/node). ## Installation Using yarn: ```sh yarn add @airbrake/browser ``` Using npm: ```sh npm install @airbrake/browser ``` Using a ` ``` Using a ` ``` ## Basic Usage First, initialize the notifier with the project ID and project key taken from [Airbrake](https://airbrake.io). To find your `project_id` and `project_key` navigate to your project's _Settings_ and copy the values from the right sidebar: ![][project-idkey] ```js import { Notifier } from '@airbrake/browser'; const airbrake = new Notifier({ projectId: 1, projectKey: 'REPLACE_ME', environment: 'production', }); ``` Then, you can send a textual message to Airbrake: ```js let promise = airbrake.notify(`user id=${user_id} not found`); promise.then((notice) => { if (notice.id) { console.log('notice id', notice.id); } else { console.log('notify failed', notice.error); } }); ``` or report errors directly: ```js try { throw new Error('Hello from Airbrake!'); } catch (err) { airbrake.notify(err); } ``` Alternatively, you can wrap any code which may throw errors using the `wrap` method: ```js let startApp = () => { throw new Error('Hello from Airbrake!'); }; startApp = airbrake.wrap(startApp); // Any exceptions thrown in startApp will be reported to Airbrake. startApp(); ``` or use the `call` shortcut: ```js let startApp = () => { throw new Error('Hello from Airbrake!'); }; airbrake.call(startApp); ``` ## Example configurations - [AngularJS](examples/angularjs) - [Angular](examples/angular) - [Legacy](examples/legacy) - [Rails](examples/rails) - [React](examples/react) - [Redux](examples/redux) - [Vue.js](examples/vuejs) ## Advanced Usage ### Notice Annotations It's possible to annotate error notices with all sorts of useful information at the time they're captured by supplying it in the object being reported. ```js try { startApp(); } catch (err) { airbrake.notify({ error: err, context: { component: 'bootstrap' }, environment: { env1: 'value' }, params: { param1: 'value' }, session: { session1: 'value' }, }); } ``` ### Severity [Severity](https://airbrake.io/docs/airbrake-faq/what-is-severity/) allows categorizing how severe an error is. By default, it's set to `error`. To redefine severity, simply overwrite `context/severity` of a notice object: ```js airbrake.notify({ error: err, context: { severity: 'warning' }, }); ``` ### Filtering errors There may be some errors thrown in your application that you're not interested in sending to Airbrake, such as errors thrown by 3rd-party libraries, or by browser extensions run by your users. The Airbrake notifier makes it simple to ignore this chaff while still processing legitimate errors. Add filters to the notifier by providing filter functions to `addFilter`. `addFilter` accepts the entire [error notice](https://airbrake.io/docs/api/#create-notice-v3) to be sent to Airbrake and provides access to the `context`, `environment`, `params`, and `session` properties. It also includes the single-element `errors` array with its `backtrace` property and associated backtrace lines. The return value of the filter function determines whether or not the error notice will be submitted. - If `null` is returned, the notice is ignored. - Otherwise, the returned notice will be submitted. An error notice must pass all provided filters to be submitted. In the following example all errors triggered by admins will be ignored: ```js airbrake.addFilter((notice) => { if (notice.params.admin) { // Ignore errors from admin sessions. return null; } return notice; }); ``` Filters can be also used to modify notice payload, e.g. to set the environment and application version: ```js airbrake.addFilter((notice) => { notice.context.environment = 'production'; notice.context.version = '1.2.3'; return notice; }); ``` ### Filtering keys #### keysBlocklist With the `keysBlocklist` option, you can specify a list of keys containing sensitive information that must be filtered out: ```js const airbrake = new Notifier({ // ... keysBlocklist: [ 'password', // exact match /secret/, // regexp match ], }); ``` #### keysAllowlist With the `keysAllowlist` option, you can specify a list of keys that should _not_ be filtered. All other keys will be substituted with the `[Filtered]` label. ```js const airbrake = new Notifier({ // ... keysAllowlist: [ 'email', // exact match /name/, // regexp match ], }); ``` ### Source maps Airbrake supports using private and public source maps. Check out our docs for more info: - [Private source maps](https://airbrake.io/docs/features/private-sourcemaps/) - [Public source maps](https://airbrake.io/docs/features/public-sourcemaps/) ### Instrumentation #### console `@airbrake/browser` automatically instruments `console.log` function calls in order to collect logs and send them with the first error. You can disable that behavior using the `instrumentation` option: ```js const airbrake = new Notifier({ // ... instrumentation: { console: false, }, }); ``` #### fetch Instruments [`fetch`][fetch] calls and sends performance statistics to Airbrake. You can disable that behavior using the `instrumentation` option: ```js const airbrake = new Notifier({ // ... instrumentation: { fetch: false, }, }); ``` #### onerror Reports the errors occurring in the Window's [error event][onerror]. You can disable that behavior using the `instrumentation` option: ```js const airbrake = new Notifier({ // ... instrumentation: { onerror: false, }, }); ``` #### history Records the history of events that led to the error and sends it to Airbrake. You can disable that behavior using the `instrumentation` option: ```js const airbrake = new Notifier({ // ... instrumentation: { history: false, }, }); ``` #### xhr Instruments [XMLHttpRequest][xhr] requests and sends performance statistics to Airbrake. You can disable that behavior using the `instrumentation` option: ```js const airbrake = new Notifier({ // ... instrumentation: { xhr: false, }, }); ``` #### unhandledrejection Instruments the [unhandledrejection][unhandledrejection] event and sends performance statistics to Airbrake. You can disable that behavior using the `instrumentation` option: ```js const airbrake = new Notifier({ // ... instrumentation: { unhandledrejection: false, }, }); ``` ### APM #### Routes ```js import { Notifier } from '@airbrake/browser'; const airbrake = new Notifier({ projectId: 1, projectKey: 'REPLACE_ME', environment: 'production', }); const routeMetric = this.airbrake.routes.start( 'GET', // HTTP method name '/abc', // Route name 200, // Status code 'application/json' // Content-Type header ); this.airbrake.routes.notify(routeMetric); ``` #### Queries ```js import { Notifier } from '@airbrake/browser'; const airbrake = new Notifier({ projectId: 1, projectKey: 'REPLACE_ME', environment: 'production', }); const queryInfo = this.airbrake.queries.start('SELECT * FROM things;'); queryInfo.file = 'file.js'; queryInfo.func = 'callerFunc'; queryInfo.line = 21; queryInfo.method = 'GET'; queryInfo.route = '/abc'; this.airbrake.queries.notify(queryInfo); ``` #### Queues ```js import { Notifier } from '@airbrake/browser'; const airbrake = new Notifier({ projectId: 1, projectKey: 'REPLACE_ME', environment: 'production', }); const queueInfo = this.airbrake.queues.start('FooWorker'); this.airbrake.queues.notify(queueInfo); ``` [project-idkey]: https://s3.amazonaws.com/airbrake-github-assets/airbrake-js/project-id-key.png [fetch]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch [onerror]: https://developer.mozilla.org/en-US/docs/Web/API/Window/error_event [xhr]: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest [unhandledrejection]: https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event ================================================ FILE: packages/browser/babel.config.js ================================================ module.exports = { presets: [['@babel/preset-env', { targets: { node: 'current' } }]], }; ================================================ FILE: packages/browser/examples/angular/README.md ================================================ # Usage with Angular ### Create an error handler The first step is to create an error handler with a `Notifier` initialized with your `projectId` and `projectKey`. In this example the handler will be in a file called `airbrake-error-handler.ts`. ```ts // src/app/airbrake-error-handler.ts import { ErrorHandler } from '@angular/core'; import { Notifier } from '@airbrake/browser'; export class AirbrakeErrorHandler implements ErrorHandler { airbrake: Notifier; constructor() { this.airbrake = new Notifier({ projectId: 1, projectKey: 'FIXME', environment: 'production' }); } handleError(error: any): void { this.airbrake.notify(error); } } ``` ### Add the error handler to your `AppModule` The last step is adding the `AirbrakeErrorHandler` to your `AppModule`, then your app will be ready to report errors to Airbrake. ```ts // src/app/app.module.ts import { BrowserModule } from '@angular/platform-browser'; import { NgModule, ErrorHandler } from '@angular/core'; import { AppComponent } from './app.component'; import { AirbrakeErrorHandler } from './airbrake-error-handler'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule ], providers: [{provide: ErrorHandler, useClass: AirbrakeErrorHandler}], bootstrap: [AppComponent] }) export class AppModule { } ``` To test that Airbrake has been installed correctly in your Angular project, just open up the JavaScript console in your internet browser and paste in: ```js window.onerror("TestError: This is a test", "path/to/file.js", 123); ``` ================================================ FILE: packages/browser/examples/angularjs/README.md ================================================ # Usage with AngularJS Integration with AngularJS is as simple as adding an [$exceptionHandler][1]: ```js // app.js const module = angular.module('app', []); module.factory('$exceptionHandler', function ($log) { const airbrake = new Airbrake.Notifier({ projectId: 1, // Airbrake project id projectKey: 'FIXME', // Airbrake project API key }); airbrake.addFilter(function (notice) { notice.context.environment = 'production'; return notice; }); return function (exception, cause) { $log.error(exception); airbrake.notify({ error: exception, params: { angular_cause: cause } }); }; }); module.controller('HelloWorldCtrl', function ($scope) { throw new Error('Uh oh, something happened'); $scope.message = 'Hello World'; }); ``` ```html Hello World

{{message}}

``` [1]: https://docs.angularjs.org/api/ng/service/$exceptionHandler ================================================ FILE: packages/browser/examples/extjs/README.md ================================================ # Usage with Ext JS ### Install the `@airbrake/browser` package ```sh npm i @airbrake/browser ``` ### Make the package available to the ExtJS framework Inside the `index.js` file located at the root of your project, require the package and set it as an Ext global property. ```js // index.js // To avoid naming conflicts with existing ExtJS properties, prepend your // package name with x // https://docs.sencha.com/extjs/7.4.0/guides/using_systems/using_npm/adding_npm_packages.html Ext.xAirbrake = require('@airbrake/browser'); ``` ### Instantiate the notifier Also in `index.js`, create a new notifier instance with your `projectId` and `projectKey`. ```js new Ext.xAirbrake.Notifier({ projectId: 1, projectKey: 'FIXME' }); ``` Airbrake will now automatically report any unhandled exceptions. If you want to send any errors manually, set the notifier instance to a variable and call `.notify()` where needed. ================================================ FILE: packages/browser/examples/legacy/README.md ================================================ # Usage with legacy applications This example loads @airbrake/browser using a `script` tag via the jsdelivr CDN. Open `index.html` in your browser to start it. ================================================ FILE: packages/browser/examples/legacy/app.js ================================================ var airbrake = new Airbrake.Notifier({ projectId: 1, projectKey: 'FIXME', }); $(function() { $('#send_error').click(function() { try { history.pushState({ foo: 'bar' }, 'Send error', 'send-error'); } catch (_) {} var val = $('#error_text').val(); throw new Error(val); }); }); try { throw new Error('Hello from Airbrake!'); } catch (err) { airbrake.notify(err).then(function(notice) { if (notice.id) { console.log('notice id:', notice.id); } else { console.log('notify failed:', notice.error); } }); } throw new Error('uncaught error'); ================================================ FILE: packages/browser/examples/legacy/index.html ================================================ Airbrake legacy example ================================================ FILE: packages/browser/examples/nextjs/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store .env* # debug npm-debug.log* yarn-debug.log* yarn-error.log* .env.local ================================================ FILE: packages/browser/examples/nextjs/README.md ================================================ # Usage with Next.js This is a sample application that can be found at [Learn Next.js](https://nextjs.org/learn). It has been adapted to include client-side and server-side error reporting with Airbrake. To run the app, run `npm install` then `npm run dev`. The app will be available at [http://localhost:3000](http://localhost:3000). Sample client-side errors are triggered with a `Throw error` button on the [homepage](http://localhost:3000) (`pages/index.js`). Sample server-side errors are triggered by visiting one of the [blog post pages](http://localhost:3000/posts/ssg-ssr) (`posts/[id].js`). ## Client-side error reporting To report client-side errors from a Next.js app, you'll need to set up and use an [`ErrorBoundary` component](https://reactjs.org/docs/error-boundaries.html), and initialize a `Notifier` with your `projectId` and `projectKey`. ```js import React from 'react'; import { Notifier } from '@airbrake/browser'; class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; this.airbrake = new Notifier({ projectId: 1, projectKey: 'FIXME' }); } componentDidCatch(error, info) { // Display fallback UI this.setState({ hasError: true }); // Send error to Airbrake this.airbrake.notify({ error: error, params: {info: info} }); } render() { if (this.state.hasError) { // You can render any custom fallback UI return

Something went wrong.

; } return this.props.children; } } export default ErrorBoundary; ``` Then, you can use it as a regular component: ```html ``` ## Server-side error reporting To report server-side errors from a Next.js app, you'll need to [override the default `Error` component](https://nextjs.org/docs/advanced-features/custom-error-page#more-advanced-error-page-customizing). Define the file `pages/_error.js` and add the following code: ```js function Error({ statusCode }) { return (

{statusCode ? `An error ${statusCode} occurred on server` : 'An error occurred on client'}

) } Error.getInitialProps = ({ res, err }) => { if (typeof window === "undefined") { const Airbrake = require('@airbrake/node') const airbrake = new Airbrake.Notifier({ projectId: 1, projectKey: 'FIXME' }); if (err) { airbrake.notify(err) } } const statusCode = res ? res.statusCode : err ? err.statusCode : 404 return { statusCode } } export default Error ``` ================================================ FILE: packages/browser/examples/nextjs/components/date.js ================================================ import { parseISO, format } from 'date-fns' export default function Date({ dateString }) { const date = parseISO(dateString) return } ================================================ FILE: packages/browser/examples/nextjs/components/error_boundary.js ================================================ import React from 'react'; import { Notifier } from '@airbrake/browser'; class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; this.airbrake = new Notifier({ projectId: process.env.NEXT_PUBLIC_AIRBRAKE_PROJECT_ID, projectKey: process.env.NEXT_PUBLIC_AIRBRAKE_PROJECT_KEY, environment: process.env.NEXT_PUBLIC_ENV }); } componentDidCatch(error, info) { // Display fallback UI this.setState({ hasError: true }); // Send error to Airbrake this.airbrake.notify({ error: error, params: {info: info} }); } render() { if (this.state.hasError) { // You can render any custom fallback UI return

Something went wrong.

; } return this.props.children; } } export default ErrorBoundary; ================================================ FILE: packages/browser/examples/nextjs/components/layout.js ================================================ import Head from 'next/head' import Image from 'next/image' import styles from './layout.module.css' import utilStyles from '../styles/utils.module.css' import Link from 'next/link' const name = 'Shu Uesugi' export const siteTitle = 'Next.js Sample Website' export default function Layout({ children, home }) { return (
{home ? ( <> {name}

{name}

) : ( <> {name}

{name}

)}
{children}
{!home && (
← Back to home
)}
) } ================================================ FILE: packages/browser/examples/nextjs/components/layout.module.css ================================================ .container { max-width: 36rem; padding: 0 1rem; margin: 3rem auto 6rem; } .header { display: flex; flex-direction: column; align-items: center; } .backToHome { margin: 3rem 0 0; } ================================================ FILE: packages/browser/examples/nextjs/lib/posts.js ================================================ import fs from 'fs' import path from 'path' import matter from 'gray-matter' import { remark } from 'remark' import html from 'remark-html' const postsDirectory = path.join(process.cwd(), 'posts') export function getSortedPostsData() { // Get file names under /posts const fileNames = fs.readdirSync(postsDirectory) const allPostsData = fileNames.map(fileName => { // Remove ".md" from file name to get id const id = fileName.replace(/\.md$/, '') // Read markdown file as string const fullPath = path.join(postsDirectory, fileName) const fileContents = fs.readFileSync(fullPath, 'utf8') // Use gray-matter to parse the post metadata section const matterResult = matter(fileContents) // Combine the data with the id return { id, ...matterResult.data } }) // Sort posts by date return allPostsData.sort((a, b) => { if (a.date < b.date) { return 1 } else { return -1 } }) } export function getAllPostIds() { const fileNames = fs.readdirSync(postsDirectory) return fileNames.map(fileName => { return { params: { id: fileName.replace(/\.md$/, '') } } }) } export async function getPostData(id) { const fullPath = path.join(postsDirectory, `${id}.md`) const fileContents = fs.readFileSync(fullPath, 'utf8') // Use gray-matter to parse the post metadata section const matterResult = matter(fileContents) // Use remark to convert markdown into HTML string const processedContent = await remark() .use(html) .process(matterResult.content) const contentHtml = processedContent.toString() // Combine the data with the id and contentHtml return { id, contentHtml, ...matterResult.data } } ================================================ FILE: packages/browser/examples/nextjs/package.json ================================================ { "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start" }, "dependencies": { "@airbrake/browser": "^2.1.7", "@airbrake/node": "^2.1.7", "date-fns": "^2.11.1", "gray-matter": "^4.0.2", "next": "latest", "react": "17.0.2", "react-dom": "17.0.2", "remark": "^14.0.1", "remark-html": "^15.0.0" } } ================================================ FILE: packages/browser/examples/nextjs/pages/_app.js ================================================ import '../styles/global.css' export default function App({ Component, pageProps }) { return } ================================================ FILE: packages/browser/examples/nextjs/pages/_error.js ================================================ function Error({ statusCode }) { return (

{statusCode ? `An error ${statusCode} occurred on server` : 'An error occurred on client'}

) } Error.getInitialProps = ({ res, err }) => { if (typeof window === "undefined") { const Airbrake = require('@airbrake/node') const airbrake = new Airbrake.Notifier({ projectId: process.env.NEXT_PUBLIC_AIRBRAKE_PROJECT_ID, projectKey: process.env.NEXT_PUBLIC_AIRBRAKE_PROJECT_KEY, }); if (err) { airbrake.notify(err) } } const statusCode = res ? res.statusCode : err ? err.statusCode : 404 return { statusCode } } export default Error ================================================ FILE: packages/browser/examples/nextjs/pages/api/hello.js ================================================ export default (req, res) => { res.status(200).json({ text: 'Hello' }) } ================================================ FILE: packages/browser/examples/nextjs/pages/index.js ================================================ import Head from 'next/head' import Layout, { siteTitle } from '../components/layout' import utilStyles from '../styles/utils.module.css' import { getSortedPostsData } from '../lib/posts' import Link from 'next/link' import Date from '../components/date' import ErrorBoundary from '../components/error_boundary' export default function Home({ allPostsData }) { return ( {siteTitle}

Hello, I’m Shu. I’m a software engineer and a translator (English/Japanese). You can contact me on{' '} Twitter.

(This is a sample website - you’ll be building a site like this in{' '} our Next.js tutorial.)

Blog

    {allPostsData.map(({ id, date, title }) => (
  • {title}
  • ))}
) } export async function getStaticProps() { const allPostsData = getSortedPostsData() return { props: { allPostsData } } } ================================================ FILE: packages/browser/examples/nextjs/pages/posts/[id].js ================================================ import Layout from '../../components/layout' import { getAllPostIds, getPostData } from '../../lib/posts' import Head from 'next/head' import Date from '../../components/date' import utilStyles from '../../styles/utils.module.css' export default function Post({ postData }) { return ( {postData.title}

{postData.title}

) } export async function getStaticPaths() { throw new Error("Server Error"); const paths = getAllPostIds() return { paths, fallback: false } } export async function getStaticProps({ params }) { const postData = await getPostData(params.id) return { props: { postData } } } ================================================ FILE: packages/browser/examples/nextjs/posts/pre-rendering.md ================================================ --- title: "Two Forms of Pre-rendering" date: "2020-01-01" --- Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. - **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request. - **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**. Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others. ================================================ FILE: packages/browser/examples/nextjs/posts/ssg-ssr.md ================================================ --- title: "When to Use Static Generation v.s. Server-side Rendering" date: "2020-01-02" --- We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request. You can use Static Generation for many types of pages, including: - Marketing pages - Blog posts - E-commerce product listings - Help and documentation You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation. On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request. In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data. ================================================ FILE: packages/browser/examples/nextjs/styles/global.css ================================================ html, body { padding: 0; margin: 0; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; line-height: 1.6; font-size: 18px; } * { box-sizing: border-box; } a { color: #0070f3; text-decoration: none; } a:hover { text-decoration: underline; } img { max-width: 100%; display: block; } ================================================ FILE: packages/browser/examples/nextjs/styles/utils.module.css ================================================ .heading2Xl { font-size: 2.5rem; line-height: 1.2; font-weight: 800; letter-spacing: -0.05rem; margin: 1rem 0; } .headingXl { font-size: 2rem; line-height: 1.3; font-weight: 800; letter-spacing: -0.05rem; margin: 1rem 0; } .headingLg { font-size: 1.5rem; line-height: 1.4; margin: 1rem 0; } .headingMd { font-size: 1.2rem; line-height: 1.5; } .borderCircle { border-radius: 9999px; } .colorInherit { color: inherit; } .padding1px { padding-top: 1px; } .list { list-style: none; padding: 0; margin: 0; } .listItem { margin: 0 0 1.25rem; } .lightText { color: #666; } ================================================ FILE: packages/browser/examples/rails/README.md ================================================ # Usage with Ruby on Rails #### Option 1 - Asset pipeline Copy the latest compiled UMD package bundle from [https://unpkg.com/@airbrake/browser](https://unpkg.com/@airbrake/browser) to `vendor/assets/javascripts/airbrake.js` in your project. Then, add the following code to your Sprockets manifest: ```javascript //= require airbrake var airbrake = new Airbrake.Notifier({ projectId: 1, projectKey: 'FIXME' }); airbrake.addFilter(function(notice) { notice.context.environment = "<%= Rails.env %>"; return notice; }); try { throw new Error('Hello from Airbrake!'); } catch (err) { airbrake.notify(err).then(function(notice) { if (notice.id) { console.log('notice id:', notice.id); } else { console.log('notify failed:', notice.error); } }); } ``` #### Option 2 - Webpacker Add `@airbrake/broswer` to your application. ```sh yarn add @airbrake/browser ``` In your main application pack, import `@airbrake/browser` and configure the client. ```js import { Notifier } from '@airbrake/browser'; const airbrake = new Notifier({ projectId: 1, projectKey: 'FIXME' }); airbrake.addFilter((notice) => { notice.context.environment = process.env.RAILS_ENV; return notice; }); try { throw new Error('Hello from Airbrake!'); } catch (err) { airbrake.notify(err).then((notice) => { if (notice.id) { console.log('notice id:', notice.id); } else { console.log('notify failed:', notice.error); } }); } ``` You should now be able to capture JavaScript exceptions in your Ruby on Rails application. ================================================ FILE: packages/browser/examples/react/README.md ================================================ # Usage with React To report errors from a React app, you'll need to set up and use an [`ErrorBoundary` component](https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html) and initialize a `Notifier` with your `projectId` and `projectKey`. ```js import React from 'react'; import { Notifier } from '@airbrake/browser'; class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; this.airbrake = new Notifier({ projectId: 1, projectKey: 'FIXME' }); } componentDidCatch(error, info) { // Display fallback UI this.setState({ hasError: true }); // Send error to Airbrake this.airbrake.notify({ error: error, params: {info: info} }); } render() { if (this.state.hasError) { // You can render any custom fallback UI return

Something went wrong.

; } return this.props.children; } } export default ErrorBoundary; ``` Then you can use it as a regular component: ```html ``` Read [Error Handling in React 16](https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html) for more details. ================================================ FILE: packages/browser/examples/redux/README.md ================================================ # Usage with Redux #### 1. Add dependencies ```bash npm install @airbrake/browser redux-airbrake --save ``` #### 2. Import dependency ```js import { Notifier } from '@airbrake/browser'; import airbrakeMiddleware from 'redux-airbrake'; ``` #### 3. Configure & add middleware ```js const airbrake = new Notifier({ projectId: '******', projectKey: '**************' }); const errorMiddleware = airbrakeMiddleware(airbrake); export const store = createStore( rootReducer, applyMiddleware( errorMiddleware ) ); export default store; ``` #### Adding notice annotations (optional) It's possible to annotate error notices with all sorts of useful information at the time they're captured by supplying it in the object being reported. ```js const errorMiddleware = airbrakeMiddleware(airbrake, { noticeAnnotations: { context: { environment: window.ENV } } }); ``` #### Adding filters Since an Airbrake instrace is passed to the middleware, you can simply add filters to the instance as described here: [https://github.com/airbrake/airbrake-js/tree/master/packages/browser#filtering-errors](https://github.com/airbrake/airbrake-js/tree/master/packages/browser#filtering-errors) For full documentation, visit [redux-airbrake](https://github.com/alexcastillo/redux-airbrake) on GitHub. ================================================ FILE: packages/browser/examples/svelte/README.md ================================================ # Usage with Svelte Integration with Svelte is as simple as adding `handleError` hooks (Server or Client): - For server error handling, add `handleError` of `HandleServerError` type - For handle error of client add `handleError` of `HandleClientError` type ## App configuration ```ts // app.d.ts // Add interface of Error // See https://kit.svelte.dev/docs/types#app // for information about these interfaces // and what to do when importing types declare global { namespace App { interface Error { message: unknown; errorId: string; } } } export {}; ``` ## Server Hook To establish an error handler for the server, use a `Notifier` with your `projectId` and `projectKey` as parameters. In this case, the handler will be located in the file `src/hooks.server.js`. ```js // src/hooks.server.js import crypto from 'crypto'; import { Notifier } from '@airbrake/browser'; var airbrake = new Notifier({ projectId: 1, // Airbrake project id projectKey: 'FIXME', // Airbrake project API key }); airbrake.addFilter(function (notice) { notice.context.hooks = 'server'; return notice; }); /** @type {import('@sveltejs/kit').HandleServerError} */ export function handleError({ error, event }) { const errorId = crypto.randomUUID(); // example integration with https://airbrake.io/ airbrake.notify({ error: error, params: { errorId: errorId, event: event }, }); return { message: error, errorId, }; } ``` ## Client Hook To establish an error handler for the client, use a `Notifier` with your `projectId` and `projectKey` as parameters. In this case, the handler will be located in the file `src/hooks.client.js`. ```js // src/hooks.client.js import crypto from 'crypto'; import { Notifier } from '@airbrake/browser'; var airbrake = new Notifier({ projectId: 1, // Airbrake project id projectKey: 'FIXME', // Airbrake project API key }); airbrake.addFilter(function (notice) { notice.context.hooks = 'client'; return notice; }); /** @type {import('@sveltejs/kit').HandleClientError} */ export function handleError({ error, event }) { const errorId = crypto.randomUUID(); // example integration with https://airbrake.io/ airbrake.notify({ error: error, params: { errorId: errorId, event: event }, }); return { message: error, errorId, }; } ``` ## Test To test that server hook has been installed correctly in your Svelte project. ```js // +page.server.js import { error } from '@sveltejs/kit'; import * as db from '$lib/server/database'; /** @type {import('./$types').PageServerLoad} */ export async function load({ params }) { const post = await db.getPost(params.slug); if (!post) { throw error(404, { message: 'Not found', }); } return { post }; } ``` To test that client hook has been installed correctly in your Svelte project, just open up the JavaScript console in your internet browser and paste in: ```js window.onerror('TestError: This is a test', 'path/to/file.js', 123); ``` ================================================ FILE: packages/browser/examples/vuejs/README.md ================================================ Usage with Vue.js ================== ### Vue 2 You can start reporting errors from your Vue 2 app by configuring an [`errorHandler`](https://vuejs.org/v2/api/#errorHandler) that uses a `Notifier` initialized with your `projectId` and `projectKey`. ```js import { Notifier } from '@airbrake/browser' var airbrake = new Notifier({ projectId: 1, projectKey: 'FIXME' }) Vue.config.errorHandler = function(err, _vm, info) { airbrake.notify({ error: err, params: {info: info} }) } ``` ### Vue 3 You can start reporting errors from your Vue 3 app by configuring an [`errorHandler`](https://v3.vuejs.org/api/application-config.html#errorhandler) that uses a `Notifier` initialized with your `projectId` and `projectKey`. ```js import { createApp } from "vue" import App from "./App.vue" import { Notifier } from '@airbrake/browser' var airbrake = new Notifier({ projectId: 1, projectKey: 'FIXME' }) let app = createApp(App) app.config.errorHandler = function(err, _vm, info) { airbrake.notify({ error: err, params: {info: info} }) } app.mount("#app") ``` ================================================ FILE: packages/browser/jest.config.js ================================================ module.exports = { transform: { '^.+\\.jsx?$': 'babel-jest', '^.+\\.tsx?$': 'ts-jest', }, testEnvironment: 'jsdom', roots: ['tests'], clearMocks: true, }; ================================================ FILE: packages/browser/package.json ================================================ { "name": "@airbrake/browser", "version": "2.1.9", "description": "Official Airbrake notifier for browsers", "author": "Airbrake", "license": "MIT", "repository": { "type": "git", "url": "git://github.com/airbrake/airbrake-js.git", "directory": "packages/browser" }, "homepage": "https://github.com/airbrake/airbrake-js/tree/master/packages/browser", "keywords": [ "exception", "error", "airbrake", "notifier" ], "dependencies": { "@types/promise-polyfill": "^6.0.3", "@types/request": "2.48.8", "cross-fetch": "^3.1.5", "error-stack-parser": "^2.0.4", "promise-polyfill": "^8.1.3", "tdigest": "^0.1.1" }, "devDependencies": { "@babel/core": "^7.9.0", "@babel/preset-env": "^7.9.0", "@rollup/plugin-commonjs": "^24.0.0", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^11.0.0", "babel-jest": "^29.3.1", "jest": "^27.3.1", "prettier": "^2.0.2", "rollup": "^2.6.1", "rollup-plugin-terser": "^7.0.0", "ts-jest": "^27.1.0", "tslib": "^2.0.0", "tslint": "^6.1.0", "tslint-config-prettier": "^1.18.0", "tslint-plugin-prettier": "^2.3.0", "typescript": "^4.0.2" }, "main": "dist/index.js", "module": "esm/index.js", "unpkg": "umd/airbrake.js", "jsdelivr": "umd/airbrake.js", "files": [ "dist/", "esm/", "umd/", "README.md", "LICENSE" ], "scripts": { "build": "yarn build:cjs && yarn build:esm && yarn build:umd", "build:watch": "concurrently 'yarn build:cjs:watch' 'yarn build:esm:watch'", "build:cjs": "tsc -p tsconfig.cjs.json", "build:cjs:watch": "tsc -p tsconfig.cjs.json -w --preserveWatchOutput", "build:esm": "tsc -p tsconfig.esm.json", "build:esm:watch": "tsc -p tsconfig.esm.json -w --preserveWatchOutput", "build:umd": "rollup --config", "clean": "rm -rf dist esm umd", "lint": "tslint -p .", "test": "jest" } } ================================================ FILE: packages/browser/rollup.config.js ================================================ import commonjs from '@rollup/plugin-commonjs'; import resolve from '@rollup/plugin-node-resolve'; import typescript from '@rollup/plugin-typescript'; import { terser } from 'rollup-plugin-terser'; const pkg = require('./package.json'); const webPlugins = [ resolve({ browser: true }), commonjs(), typescript({ tsconfig: './tsconfig.umd.json' }), ]; function umd(cfg) { return Object.assign( { format: 'umd', banner: `/* airbrake-js v${pkg.version} */`, sourcemap: true, name: 'Airbrake', }, cfg, ); } export default { input: 'src/index.ts', output: [ umd({ file: 'umd/airbrake.js' }), umd({ file: 'umd/airbrake.min.js', plugins: [terser()] }), ], plugins: webPlugins, }; ================================================ FILE: packages/browser/src/base_notifier.ts ================================================ import Promise from 'promise-polyfill'; import { IFuncWrapper } from './func_wrapper'; import { jsonifyNotice } from './jsonify_notice'; import { INotice } from './notice'; import { Scope } from './scope'; import { espProcessor } from './processor/esp'; import { Processor } from './processor/processor'; import { angularMessageFilter } from './filter/angular_message'; import { makeDebounceFilter } from './filter/debounce'; import { Filter } from './filter/filter'; import { ignoreNoiseFilter } from './filter/ignore_noise'; import { uncaughtMessageFilter } from './filter/uncaught_message'; import { makeRequester, Requester } from './http_req'; import { IOptions } from './options'; import { QueriesStats } from './queries'; import { QueueMetric, QueuesStats } from './queues'; import { RouteMetric, RoutesBreakdowns, RoutesStats } from './routes'; import { NOTIFIER_NAME, NOTIFIER_VERSION, NOTIFIER_URL } from './version'; import { PerformanceFilter } from './filter/performance_filter'; import { RemoteSettings } from './remote_settings'; export class BaseNotifier { routes: Routes; queues: Queues; queries: QueriesStats; _opt: IOptions; _url: string; _processor: Processor; _requester: Requester; _filters: Filter[] = []; _performanceFilters: PerformanceFilter[] = []; _scope = new Scope(); _onClose: (() => void)[] = []; constructor(opt: IOptions) { if (!opt.projectId || !opt.projectKey) { throw new Error('airbrake: projectId and projectKey are required'); } this._opt = opt; this._opt.host = this._opt.host || 'https://api.airbrake.io'; this._opt.remoteConfigHost = this._opt.remoteConfigHost || 'https://notifier-configs.airbrake.io'; this._opt.apmHost = this._opt.apmHost || 'https://api.airbrake.io'; this._opt.timeout = this._opt.timeout || 10000; this._opt.keysBlocklist = this._opt.keysBlocklist || [/password/, /secret/]; this._url = `${this._opt.host}/api/v3/projects/${this._opt.projectId}/notices?key=${this._opt.projectKey}`; this._opt.errorNotifications = this._opt.errorNotifications !== false; this._opt.performanceStats = this._opt.performanceStats !== false; this._opt.queryStats = this._opt.queryStats !== false; this._opt.queueStats = this._opt.queueStats !== false; this._opt.remoteConfig = this._opt.remoteConfig !== false; this._processor = this._opt.processor || espProcessor; this._requester = makeRequester(this._opt); this.addFilter(ignoreNoiseFilter); this.addFilter(makeDebounceFilter()); this.addFilter(uncaughtMessageFilter); this.addFilter(angularMessageFilter); this.addFilter((notice: INotice): INotice | null => { notice.context.notifier = { name: NOTIFIER_NAME, version: NOTIFIER_VERSION, url: NOTIFIER_URL, }; if (this._opt.environment) { notice.context.environment = this._opt.environment; } return notice; }); this.routes = new Routes(this); this.queues = new Queues(this); this.queries = new QueriesStats(this._opt); if (this._opt.remoteConfig) { const pollerId = new RemoteSettings(this._opt).poll(); this._onClose.push(() => clearInterval(pollerId)); } } close(): void { for (let fn of this._onClose) { fn(); } } scope(): Scope { return this._scope; } setActiveScope(scope: Scope) { this._scope = scope; } addFilter(filter: Filter): void { this._filters.push(filter); } addPerformanceFilter(performanceFilter: PerformanceFilter) { this._performanceFilters.push(performanceFilter); } notify(err: any): Promise { if (typeof err !== 'object' || err === null || !('error' in err)) { err = { error: err }; } this.handleFalseyError(err); let notice = this.newNotice(err); if (!this._opt.errorNotifications) { notice.error = new Error( `airbrake: not sending this error, errorNotifications is disabled err=${JSON.stringify( err.error )}` ); return Promise.resolve(notice); } let error = this._processor(err.error); notice.errors.push(error); for (let filter of this._filters) { let r = filter(notice); if (r === null) { notice.error = new Error('airbrake: error is filtered'); return Promise.resolve(notice); } notice = r; } if (!notice.context) { notice.context = {}; } notice.context.language = 'JavaScript'; return this._sendNotice(notice); } private handleFalseyError(err: any) { if (Number.isNaN(err.error)) { err.error = new Error('NaN'); } else if (err.error === undefined) { err.error = new Error('undefined'); } else if (err.error === '') { err.error = new Error(''); } else if (err.error === null) { err.error = new Error('null'); } } private newNotice(err: any): INotice { return { errors: [], context: { severity: 'error', ...this.scope().context(), ...err.context, }, params: err.params || {}, environment: err.environment || {}, session: err.session || {}, }; } _sendNotice(notice: INotice): Promise { let body = jsonifyNotice(notice, { keysBlocklist: this._opt.keysBlocklist, }); if (this._opt.reporter) { if (typeof this._opt.reporter === 'function') { return this._opt.reporter(notice); } else { console.warn('airbrake: options.reporter must be a function'); } } let req = { method: 'POST', url: this._url, body, }; return this._requester(req) .then((resp) => { notice.id = resp.json.id; notice.url = resp.json.url; return notice; }) .catch((err) => { notice.error = err; return notice; }); } wrap(fn, props: string[] = []): IFuncWrapper { if (fn._airbrake) { return fn; } // tslint:disable-next-line:no-this-assignment let client = this; let airbrakeWrapper = function () { let fnArgs = Array.prototype.slice.call(arguments); let wrappedArgs = client._wrapArguments(fnArgs); try { return fn.apply(this, wrappedArgs); } catch (err) { client.notify({ error: err, params: { arguments: fnArgs } }); client._ignoreNextWindowError(); throw err; } } as IFuncWrapper; for (let prop in fn) { if (fn.hasOwnProperty(prop)) { airbrakeWrapper[prop] = fn[prop]; } } for (let prop of props) { if (fn.hasOwnProperty(prop)) { airbrakeWrapper[prop] = fn[prop]; } } airbrakeWrapper._airbrake = true; airbrakeWrapper.inner = fn; return airbrakeWrapper; } _wrapArguments(args: any[]): any[] { for (let i = 0; i < args.length; i++) { let arg = args[i]; if (typeof arg === 'function') { args[i] = this.wrap(arg); } } return args; } _ignoreNextWindowError() {} call(fn, ..._args: any[]): any { let wrapper = this.wrap(fn); return wrapper.apply(this, Array.prototype.slice.call(arguments, 1)); } } class Routes { _notifier: BaseNotifier; _routes: RoutesStats; _breakdowns: RoutesBreakdowns; _opt: IOptions; constructor(notifier: BaseNotifier) { this._notifier = notifier; this._routes = new RoutesStats(notifier._opt); this._breakdowns = new RoutesBreakdowns(notifier._opt); this._opt = notifier._opt; } start( method = '', route = '', statusCode = 0, contentType = '' ): RouteMetric { const metric = new RouteMetric(method, route, statusCode, contentType); if (!this._opt.performanceStats) { return metric; } const scope = this._notifier.scope().clone(); scope.setContext({ httpMethod: method, route }); scope.setRouteMetric(metric); this._notifier.setActiveScope(scope); return metric; } notify(req: RouteMetric): void { if (!this._opt.performanceStats) { return; } req.end(); for (const performanceFilter of this._notifier._performanceFilters) { if (performanceFilter(req) === null) { return; } } this._routes.notify(req); this._breakdowns.notify(req); } } class Queues { _notifier: BaseNotifier; _queues: QueuesStats; _opt: IOptions; constructor(notifier: BaseNotifier) { this._notifier = notifier; this._queues = new QueuesStats(notifier._opt); this._opt = notifier._opt; } start(queue: string): QueueMetric { const metric = new QueueMetric(queue); if (!this._opt.performanceStats) { return metric; } const scope = this._notifier.scope().clone(); scope.setContext({ queue }); scope.setQueueMetric(metric); this._notifier.setActiveScope(scope); return metric; } notify(q: QueueMetric): void { if (!this._opt.performanceStats) { return; } q.end(); this._queues.notify(q); } } ================================================ FILE: packages/browser/src/filter/angular_message.ts ================================================ import { INotice } from '../notice'; let re = new RegExp( [ '^', '\\[(\\$.+)\\]', // type '\\s', '([\\s\\S]+)', // message '$', ].join('') ); export function angularMessageFilter(notice: INotice): INotice { let err = notice.errors[0]; if (err.type !== '' && err.type !== 'Error') { return notice; } let m = err.message.match(re); if (m !== null) { err.type = m[1]; err.message = m[2]; } return notice; } ================================================ FILE: packages/browser/src/filter/debounce.ts ================================================ import { INotice } from '../notice'; import { Filter } from './filter'; export function makeDebounceFilter(): Filter { let lastNoticeJSON: string; let timeout; return (notice: INotice): INotice | null => { let s = JSON.stringify(notice.errors); if (s === lastNoticeJSON) { return null; } if (timeout) { clearTimeout(timeout); } lastNoticeJSON = s; timeout = setTimeout(() => { lastNoticeJSON = ''; }, 1000); return notice; }; } ================================================ FILE: packages/browser/src/filter/filter.ts ================================================ import { INotice } from '../notice'; export type Filter = (notice: INotice) => INotice | null; ================================================ FILE: packages/browser/src/filter/ignore_noise.ts ================================================ import { INotice } from '../notice'; const IGNORED_MESSAGES = [ 'Script error', 'Script error.', 'InvalidAccessError', ]; export function ignoreNoiseFilter(notice: INotice): INotice | null { let err = notice.errors[0]; if (err.type === '' && IGNORED_MESSAGES.indexOf(err.message) !== -1) { return null; } if (err.backtrace && err.backtrace.length > 0) { let frame = err.backtrace[0]; if (frame.file === '') { return null; } } return notice; } ================================================ FILE: packages/browser/src/filter/performance_filter.ts ================================================ import { RouteMetric } from '../routes'; export type PerformanceFilter = (metric: RouteMetric) => RouteMetric | null; ================================================ FILE: packages/browser/src/filter/uncaught_message.ts ================================================ import { INotice } from '../notice'; let re = new RegExp( [ '^', 'Uncaught\\s', '(.+?)', // type ':\\s', '(.+)', // message '$', ].join('') ); export function uncaughtMessageFilter(notice: INotice): INotice { let err = notice.errors[0]; if (err.type !== '' && err.type !== 'Error') { return notice; } let m = err.message.match(re); if (m !== null) { err.type = m[1]; err.message = m[2]; } return notice; } ================================================ FILE: packages/browser/src/filter/window.ts ================================================ import { INotice } from '../notice'; export function windowFilter(notice: INotice): INotice { if (window.navigator && window.navigator.userAgent) { notice.context.userAgent = window.navigator.userAgent; } if (window.location) { notice.context.url = String(window.location); // Set root directory to group errors on different subdomains together. notice.context.rootDirectory = window.location.protocol + '//' + window.location.host; } return notice; } ================================================ FILE: packages/browser/src/func_wrapper.ts ================================================ export interface IFuncWrapper { (): any; inner: () => any; _airbrake?: boolean; } ================================================ FILE: packages/browser/src/http_req/api.ts ================================================ export interface IHttpRequest { method: string; url: string; body?: string; timeout?: number; headers?: any; } export interface IHttpResponse { json: any; } export type Requester = (req: IHttpRequest) => Promise; export let errors = { unauthorized: new Error( 'airbrake: unauthorized: project id or key are wrong' ), ipRateLimited: new Error('airbrake: IP is rate limited'), }; ================================================ FILE: packages/browser/src/http_req/fetch.ts ================================================ import fetch from 'cross-fetch'; import Promise from 'promise-polyfill'; import { errors, IHttpRequest, IHttpResponse } from './api'; let rateLimitReset = 0; export function request(req: IHttpRequest): Promise { let utime = Date.now() / 1000; if (utime < rateLimitReset) { return Promise.reject(errors.ipRateLimited); } let opt = { method: req.method, body: req.body, headers: req.headers, }; return fetch(req.url, opt).then((resp: Response) => { if (resp.status === 401) { throw errors.unauthorized; } if (resp.status === 429) { let s = resp.headers.get('X-RateLimit-Delay'); if (!s) { throw errors.ipRateLimited; } let n = parseInt(s, 10); if (n > 0) { rateLimitReset = Date.now() / 1000 + n; } throw errors.ipRateLimited; } if (resp.status === 204) { return { json: null }; } if (resp.status === 404) { throw new Error('404 Not Found'); } if (resp.status >= 200 && resp.status < 300) { return resp.json().then((json) => { return { json }; }); } if (resp.status >= 400 && resp.status < 500) { return resp.json().then((json) => { let err = new Error(json.message); throw err; }); } return resp.text().then((body) => { let err = new Error( `airbrake: fetch: unexpected response: code=${resp.status} body='${body}'` ); throw err; }); }); } ================================================ FILE: packages/browser/src/http_req/index.ts ================================================ import { IOptions } from '../options'; import { Requester } from './api'; import { request as fetchRequest } from './fetch'; import { makeRequester as makeNodeRequester } from './node'; export { Requester }; export function makeRequester(opts: IOptions): Requester { if (opts.request) { return makeNodeRequester(opts.request); } return fetchRequest; } ================================================ FILE: packages/browser/src/http_req/node.ts ================================================ import * as request_lib from 'request'; import Promise from 'promise-polyfill'; import { errors, IHttpRequest, IHttpResponse, Requester } from './api'; type requestAPI = request_lib.RequestAPI< request_lib.Request, request_lib.CoreOptions, request_lib.RequiredUriUrl >; export function makeRequester(api: requestAPI): Requester { return (req: IHttpRequest): Promise => { return request(req, api); }; } let rateLimitReset = 0; function request(req: IHttpRequest, api: requestAPI): Promise { let utime = Date.now() / 1000; if (utime < rateLimitReset) { return Promise.reject(errors.ipRateLimited); } return new Promise((resolve, reject) => { api( { url: req.url, method: req.method, body: req.body, headers: { 'content-type': 'application/json', }, timeout: req.timeout, }, (error: any, resp: request_lib.RequestResponse, body: any): void => { if (error) { reject(error); return; } if (!resp.statusCode) { error = new Error( `airbrake: request: response statusCode is ${resp.statusCode}` ); reject(error); return; } if (resp.statusCode === 401) { reject(errors.unauthorized); return; } if (resp.statusCode === 429) { reject(errors.ipRateLimited); let h = resp.headers['x-ratelimit-delay']; if (!h) { return; } let s: string; if (typeof h === 'string') { s = h; } else if (h instanceof Array) { s = h[0]; } else { return; } let n = parseInt(s, 10); if (n > 0) { rateLimitReset = Date.now() / 1000 + n; } return; } if (resp.statusCode === 204) { resolve({ json: null }); return; } if (resp.statusCode >= 200 && resp.statusCode < 300) { let json; try { json = JSON.parse(body); } catch (err) { reject(err); return; } resolve(json); return; } if (resp.statusCode >= 400 && resp.statusCode < 500) { let json; try { json = JSON.parse(body); } catch (err) { reject(err); return; } error = new Error(json.message); reject(error); return; } body = body.trim(); error = new Error( `airbrake: node: unexpected response: code=${resp.statusCode} body='${body}'` ); reject(error); } ); }); } ================================================ FILE: packages/browser/src/index.ts ================================================ export { Notifier } from './notifier'; export { BaseNotifier } from './base_notifier'; export { INotice } from './notice'; export { IOptions } from './options'; export { QueryInfo } from './queries'; export { Scope } from './scope'; ================================================ FILE: packages/browser/src/instrumentation/console.ts ================================================ import { IFuncWrapper } from '../func_wrapper'; import { Notifier } from '../notifier'; const CONSOLE_METHODS = ['debug', 'log', 'info', 'warn', 'error']; export function instrumentConsole(notifier: Notifier): void { // tslint:disable-next-line:no-this-assignment for (let m of CONSOLE_METHODS) { if (!(m in console)) { continue; } const oldFn = console[m]; let newFn = ((...args) => { oldFn.apply(console, args); notifier.scope().pushHistory({ type: 'log', severity: m, arguments: args, }); }) as IFuncWrapper; newFn.inner = oldFn; console[m] = newFn; } } ================================================ FILE: packages/browser/src/instrumentation/dom.ts ================================================ import { Notifier } from '../notifier'; const elemAttrs = ['type', 'name', 'src']; export function instrumentDOM(notifier: Notifier) { const handler = makeEventHandler(notifier); if (window.addEventListener) { window.addEventListener('load', handler); window.addEventListener( 'error', (event: Event): void => { if (getProp(event, 'error')) { return; } handler(event); }, true ); } if (typeof document === 'object' && document.addEventListener) { document.addEventListener('DOMContentLoaded', handler); document.addEventListener('click', handler); document.addEventListener('keypress', handler); } } function makeEventHandler(notifier: Notifier): EventListener { return (event: Event): void => { let target = getProp(event, 'target') as HTMLElement | null; if (!target) { return; } let state: any = { type: event.type }; try { state.target = elemPath(target); } catch (err) { state.target = `<${String(err)}>`; } notifier.scope().pushHistory(state); }; } function elemName(elem: HTMLElement): string { if (!elem) { return ''; } let s: string[] = []; if (elem.tagName) { s.push(elem.tagName.toLowerCase()); } if (elem.id) { s.push('#'); s.push(elem.id); } if (elem.classList && Array.from) { s.push('.'); s.push(Array.from(elem.classList).join('.')); } else if (elem.className) { let str = classNameString(elem.className); if (str !== '') { s.push('.'); s.push(str); } } if (elem.getAttribute) { for (let attr of elemAttrs) { let value = elem.getAttribute(attr); if (value) { s.push(`[${attr}="${value}"]`); } } } return s.join(''); } function classNameString(name: any): string { if (name.split) { return name.split(' ').join('.'); } if (name.baseVal && name.baseVal.split) { // SVGAnimatedString return name.baseVal.split(' ').join('.'); } console.error('unsupported HTMLElement.className type', typeof name); return ''; } function elemPath(elem: HTMLElement): string { const maxLen = 10; let path: string[] = []; let parent = elem; while (parent) { let name = elemName(parent); if (name !== '') { path.push(name); if (path.length > maxLen) { break; } } parent = parent.parentNode as HTMLElement; } if (path.length === 0) { return String(elem); } return path.reverse().join(' > '); } function getProp(obj: any, prop: string): any { try { return obj[prop]; } catch (_) { // Permission denied to access property return null; } } ================================================ FILE: packages/browser/src/instrumentation/fetch.ts ================================================ import { Notifier } from '../notifier'; export function instrumentFetch(notifier: Notifier): void { // tslint:disable-next-line:no-this-assignment let oldFetch = window.fetch; window.fetch = function ( req: RequestInfo, options?: RequestInit ): Promise { let state: any = { type: 'xhr', date: new Date(), }; state.method = options && options.method ? options.method : 'GET'; if (typeof req === 'string') { state.url = req; } else { state.method = req.method; state.url = req.url; } // Some platforms (e.g. react-native) implement fetch via XHR. notifier._ignoreNextXHR++; setTimeout(() => notifier._ignoreNextXHR--); return oldFetch .apply(this, arguments) .then((resp: Response) => { state.statusCode = resp.status; state.duration = new Date().getTime() - state.date.getTime(); notifier.scope().pushHistory(state); return resp; }) .catch((err) => { state.error = err; state.duration = new Date().getTime() - state.date.getTime(); notifier.scope().pushHistory(state); throw err; }); }; } ================================================ FILE: packages/browser/src/instrumentation/location.ts ================================================ import { Notifier } from '../notifier'; let lastLocation = ''; // In some environments (i.e. Cypress) document.location may sometimes be null function getCurrentLocation(): string | null { return document.location && document.location.pathname; } export function instrumentLocation(notifier: Notifier): void { lastLocation = getCurrentLocation(); const oldFn = window.onpopstate; window.onpopstate = function abOnpopstate(_event: PopStateEvent): any { const url = getCurrentLocation(); if (url) { recordLocation(notifier, url); } if (oldFn) { return oldFn.apply(this, arguments); } }; const oldPushState = history.pushState; history.pushState = function abPushState( _state: any, _title: string, url?: string | null ): void { if (url) { recordLocation(notifier, url.toString()); } oldPushState.apply(this, arguments); }; } function recordLocation(notifier: Notifier, url: string): void { let index = url.indexOf('://'); if (index >= 0) { url = url.slice(index + 3); index = url.indexOf('/'); url = index >= 0 ? url.slice(index) : '/'; } else if (url.charAt(0) !== '/') { url = '/' + url; } notifier.scope().pushHistory({ type: 'location', from: lastLocation, to: url, }); lastLocation = url; } ================================================ FILE: packages/browser/src/instrumentation/unhandledrejection.ts ================================================ import { Notifier } from '../notifier'; export function instrumentUnhandledrejection(notifier: Notifier): void { const handler = onUnhandledrejection.bind(notifier); window.addEventListener('unhandledrejection', handler); notifier._onClose.push(() => { window.removeEventListener('unhandledrejection', handler); }); } function onUnhandledrejection(e: any): void { // Handle native or bluebird Promise rejections // https://developer.mozilla.org/en-US/docs/Web/Events/unhandledrejection // http://bluebirdjs.com/docs/api/error-management-configuration.html let reason = e.reason || (e.detail && e.detail.reason); if (!reason) return; let msg = reason.message || String(reason); if (msg.indexOf && msg.indexOf('airbrake: ') === 0) return; if (typeof reason !== 'object' || reason.error === undefined) { this.notify({ error: reason, context: { unhandledRejection: true, }, }); return; } this.notify({ ...reason, context: { unhandledRejection: true } }); } ================================================ FILE: packages/browser/src/instrumentation/xhr.ts ================================================ import { Notifier } from '../notifier'; interface IXMLHttpRequestWithState extends XMLHttpRequest { __state: any; } export function instrumentXHR(notifier: Notifier): void { function recordReq(req: IXMLHttpRequestWithState): void { const state = req.__state; state.statusCode = req.status; state.duration = new Date().getTime() - state.date.getTime(); notifier.scope().pushHistory(state); } const oldOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function abOpen( method: string, url: string, _async?: boolean, _user?: string, _password?: string ): void { if (notifier._ignoreNextXHR === 0) { this.__state = { type: 'xhr', method, url, }; } oldOpen.apply(this, arguments); }; const oldSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function abSend(_data?: any): void { let oldFn = this.onreadystatechange; this.onreadystatechange = function (_ev: Event): any { if (this.readyState === 4 && this.__state) { recordReq(this); } if (oldFn) { return oldFn.apply(this, arguments); } }; if (this.__state) { (this as IXMLHttpRequestWithState).__state.date = new Date(); } return oldSend.apply(this, arguments); }; } ================================================ FILE: packages/browser/src/jsonify_notice.ts ================================================ import { INotice } from './notice'; const FILTERED = '[Filtered]'; const MAX_OBJ_LENGTH = 128; // jsonifyNotice serializes notice to JSON and truncates params, // environment and session keys. export function jsonifyNotice( notice: INotice, { maxLength = 64000, keysBlocklist = [], keysAllowlist = [] } = {} ): string { if (notice.errors) { for (let i = 0; i < notice.errors.length; i++) { let t = new Truncator({ keysBlocklist, keysAllowlist }); notice.errors[i] = t.truncate(notice.errors[i]); } } let s = ''; let keys = ['params', 'environment', 'session']; for (let level = 0; level < 8; level++) { let opts = { level, keysBlocklist, keysAllowlist }; for (let key of keys) { let obj = notice[key]; if (obj) { notice[key] = truncate(obj, opts); } } s = JSON.stringify(notice); if (s.length < maxLength) { return s; } } let params = { json: s.slice(0, Math.floor(maxLength / 2)) + '...', }; keys.push('errors'); for (let key of keys) { let obj = notice[key]; if (!obj) { continue; } s = JSON.stringify(obj); params[key] = s.length; } let err = new Error( `airbrake: notice exceeds max length and can't be truncated` ); (err as any).params = params; throw err; } function scale(num: number, level: number): number { return num >> level || 1; } interface ITruncatorOptions { level?: number; keysBlocklist?: any[]; keysAllowlist?: any[]; } class Truncator { private maxStringLength = 1024; private maxObjectLength = MAX_OBJ_LENGTH; private maxArrayLength = MAX_OBJ_LENGTH; private maxDepth = 8; private keys: string[] = []; private keysBlocklist: any[] = []; private keysAllowlist: any[] = []; private seen: any[] = []; constructor(opts: ITruncatorOptions) { let level = opts.level || 0; this.keysBlocklist = opts.keysBlocklist || []; this.keysAllowlist = opts.keysAllowlist || []; this.maxStringLength = scale(this.maxStringLength, level); this.maxObjectLength = scale(this.maxObjectLength, level); this.maxArrayLength = scale(this.maxArrayLength, level); this.maxDepth = scale(this.maxDepth, level); } public truncate(value: any, key = '', depth = 0): any { if (value === null || value === undefined) { return value; } switch (typeof value) { case 'boolean': case 'number': case 'function': return value; case 'string': return this.truncateString(value); case 'object': break; default: return this.truncateString(String(value)); } if (value instanceof String) { return this.truncateString(value.toString()); } if ( value instanceof Boolean || value instanceof Number || value instanceof Date || value instanceof RegExp ) { return value; } if (value instanceof Error) { return this.truncateString(value.toString()); } if (this.seen.indexOf(value) >= 0) { return `[Circular ${this.getPath(value)}]`; } let type = objectType(value); depth++; if (depth > this.maxDepth) { return `[Truncated ${type}]`; } this.keys.push(key); this.seen.push(value); switch (type) { case 'Array': return this.truncateArray(value, depth); case 'Object': return this.truncateObject(value, depth); default: let saved = this.maxDepth; this.maxDepth = 0; let obj = this.truncateObject(value, depth); obj.__type = type; this.maxDepth = saved; return obj; } } private getPath(value): string { let index = this.seen.indexOf(value); let path = [this.keys[index]]; for (let i = index; i >= 0; i--) { let sub = this.seen[i]; if (sub && getAttr(sub, path[0]) === value) { value = sub; path.unshift(this.keys[i]); } } return '~' + path.join('.'); } private truncateString(s: string): string { if (s.length > this.maxStringLength) { return s.slice(0, this.maxStringLength) + '...'; } return s; } private truncateArray(arr: any[], depth = 0): any[] { let length = 0; let dst: any = []; for (let i = 0; i < arr.length; i++) { let el = arr[i]; dst.push(this.truncate(el, i.toString(), depth)); length++; if (length >= this.maxArrayLength) { break; } } return dst; } private truncateObject(obj: any, depth = 0): any { let length = 0; let dst = {}; for (let key in obj) { if (!Object.prototype.hasOwnProperty.call(obj, key)) { continue; } if (this.filterKey(key, dst)) continue; let value = getAttr(obj, key); if (value === undefined || typeof value === 'function') { continue; } dst[key] = this.truncate(value, key, depth); length++; if (length >= this.maxObjectLength) { break; } } return dst; } private filterKey(key: string, obj: any): boolean { if ( (this.keysAllowlist.length > 0 && !isInList(key, this.keysAllowlist)) || (this.keysAllowlist.length === 0 && isInList(key, this.keysBlocklist)) ) { obj[key] = FILTERED; return true; } return false; } } export function truncate(value: any, opts: ITruncatorOptions = {}): any { let t = new Truncator(opts); return t.truncate(value); } function getAttr(obj: any, attr: string): any { // Ignore browser specific exception trying to read an attribute (#79). try { return obj[attr]; } catch (_) { return; } } function objectType(obj: any): string { let s = Object.prototype.toString.apply(obj); return s.slice('[object '.length, -1); } function isInList(key: string, list: any[]): boolean { for (let v of list) { if (v === key) { return true; } if (v instanceof RegExp) { if (key.match(v)) { return true; } } } return false; } ================================================ FILE: packages/browser/src/metrics.ts ================================================ export interface IMetric { isRecording(): boolean; startSpan(name: string, startTime?: Date): void; endSpan(name: string, endTime?: Date): void; _incGroup(name: string, ms: number): void; } export class Span { _metric: IMetric; name: string; startTime: Date; endTime: Date; _dur = 0; _level = 0; constructor(metric: IMetric, name: string, startTime?: Date) { this._metric = metric; this.name = name; this.startTime = startTime || new Date(); } end(endTime?: Date) { this.endTime = endTime ? endTime : new Date(); this._dur += this.endTime.getTime() - this.startTime.getTime(); this._metric._incGroup(this.name, this._dur); this._metric = null; } _pause() { if (this._paused()) { return; } let now = new Date(); this._dur += now.getTime() - this.startTime.getTime(); this.startTime = null; } _resume() { if (!this._paused()) { return; } this.startTime = new Date(); } _paused() { return this.startTime == null; } } export class BaseMetric implements IMetric { startTime: Date; endTime: Date; _spans = {}; _groups = {}; constructor() { this.startTime = new Date(); } end(endTime?: Date): void { if (!this.endTime) { this.endTime = endTime || new Date(); } } isRecording(): boolean { return true; } startSpan(name: string, startTime?: Date): void { let span = this._spans[name]; if (span) { span._level++; } else { span = new Span(this, name, startTime); this._spans[name] = span; } } endSpan(name: string, endTime?: Date): void { let span = this._spans[name]; if (!span) { console.error('airbrake: span=%s does not exist', name); return; } if (span._level > 0) { span._level--; } else { span.end(endTime); delete this._spans[span.name]; } } _incGroup(name: string, ms: number): void { this._groups[name] = (this._groups[name] || 0) + ms; } _duration(): number { if (!this.endTime) { this.endTime = new Date(); } return this.endTime.getTime() - this.startTime.getTime(); } } export class NoopMetric implements IMetric { isRecording(): boolean { return false; } startSpan(_name: string, _startTime?: Date): void {} endSpan(_name: string, _startTime?: Date): void {} _incGroup(_name: string, _ms: number): void {} } ================================================ FILE: packages/browser/src/notice.ts ================================================ export interface INoticeFrame { function: string; file: string; line: number; column: number; } export interface INoticeError { type: string; message: string; backtrace: INoticeFrame[]; } export interface INotice { id?: string; url?: string; error?: Error; errors?: INoticeError[]; context?: any; params?: any; session?: any; environment?: any; } ================================================ FILE: packages/browser/src/notifier.ts ================================================ import Promise from 'promise-polyfill'; import { BaseNotifier } from './base_notifier'; import { windowFilter } from './filter/window'; import { instrumentConsole } from './instrumentation/console'; import { instrumentDOM } from './instrumentation/dom'; import { instrumentFetch } from './instrumentation/fetch'; import { instrumentLocation } from './instrumentation/location'; import { instrumentXHR } from './instrumentation/xhr'; import { instrumentUnhandledrejection } from './instrumentation/unhandledrejection'; import { INotice } from './notice'; import { IInstrumentationOptions, IOptions } from './options'; interface ITodo { err: any; resolve: (notice: INotice) => void; reject: (err: Error) => void; } export class Notifier extends BaseNotifier { protected offline = false; protected todo: ITodo[] = []; _ignoreWindowError = 0; _ignoreNextXHR = 0; constructor(opt: IOptions) { super(opt); if (typeof window === 'undefined') { return; } this.addFilter(windowFilter); if (window.addEventListener) { this.onOnline = this.onOnline.bind(this); window.addEventListener('online', this.onOnline); this.onOffline = this.onOffline.bind(this); window.addEventListener('offline', this.onOffline); this._onClose.push(() => { window.removeEventListener('online', this.onOnline); window.removeEventListener('offline', this.onOffline); }); } this._instrument(opt.instrumentation); } _instrument(opt: IInstrumentationOptions = {}) { if (opt.console === undefined) { opt.console = !isDevEnv(this._opt.environment); } if (enabled(opt.onerror)) { // tslint:disable-next-line:no-this-assignment let self = this; let oldHandler = window.onerror; window.onerror = function abOnerror() { if (oldHandler) { oldHandler.apply(this, arguments); } self.onerror.apply(self, arguments); }; } instrumentDOM(this); if (enabled(opt.fetch) && typeof fetch === 'function') { instrumentFetch(this); } if (enabled(opt.history) && typeof history === 'object') { instrumentLocation(this); } if (enabled(opt.console) && typeof console === 'object') { instrumentConsole(this); } if (enabled(opt.xhr) && typeof XMLHttpRequest !== 'undefined') { instrumentXHR(this); } if ( enabled(opt.unhandledrejection) && typeof addEventListener === 'function' ) { instrumentUnhandledrejection(this); } } public notify(err: any): Promise { if (this.offline) { return new Promise((resolve, reject) => { this.todo.push({ err, resolve, reject, }); while (this.todo.length > 100) { let j = this.todo.shift(); if (j === undefined) { break; } j.resolve({ error: new Error('airbrake: offline queue is too large'), }); } }); } return super.notify(err); } protected onOnline(): void { this.offline = false; for (let j of this.todo) { this.notify(j.err).then((notice) => { j.resolve(notice); }); } this.todo = []; } protected onOffline(): void { this.offline = true; } onerror( message: string, filename?: string, line?: number, column?: number, err?: Error ): void { if (this._ignoreWindowError > 0) { return; } if (err) { this.notify({ error: err, context: { windowError: true, }, }); return; } // Ignore errors without file or line. if (!filename || !line) { return; } this.notify({ error: { message, fileName: filename, lineNumber: line, columnNumber: column, noStack: true, }, context: { windowError: true, }, }); } _ignoreNextWindowError(): void { this._ignoreWindowError++; setTimeout(() => this._ignoreWindowError--); } } function isDevEnv(env: any): boolean { return env && env.startsWith && env.startsWith('dev'); } function enabled(v: undefined | boolean): boolean { return v === undefined || v === true; } ================================================ FILE: packages/browser/src/options.ts ================================================ import * as request from 'request'; import { INotice } from './notice'; import { Processor } from './processor/processor'; type Reporter = (notice: INotice) => Promise; export interface IInstrumentationOptions { onerror?: boolean; fetch?: boolean; history?: boolean; console?: boolean; xhr?: boolean; unhandledrejection?: boolean; } export interface IOptions { projectId: number; projectKey: string; environment?: string; host?: string; apmHost?: string; remoteConfigHost?: string; remoteConfig?: boolean; timeout?: number; keysBlocklist?: any[]; processor?: Processor; reporter?: Reporter; instrumentation?: IInstrumentationOptions; errorNotifications?: boolean; performanceStats?: boolean; queryStats?: boolean; queueStats?: boolean; request?: request.RequestAPI< request.Request, request.CoreOptions, request.RequiredUriUrl >; } ================================================ FILE: packages/browser/src/processor/esp.ts ================================================ import { INoticeError, INoticeFrame } from '../notice'; import ErrorStackParser from 'error-stack-parser'; const hasConsole = typeof console === 'object' && console.warn; export interface IStackFrame { functionName?: string; fileName?: string; lineNumber?: number; columnNumber?: number; } export interface IError extends Error, IStackFrame { noStack?: boolean; } function parse(err: IError): IStackFrame[] { try { return ErrorStackParser.parse(err); } catch (parseErr) { if (hasConsole && err.stack) { console.warn('ErrorStackParser:', parseErr.toString(), err.stack); } } if (err.fileName) { return [err]; } return []; } export function espProcessor(err: IError): INoticeError { let backtrace: INoticeFrame[] = []; if (err.noStack) { backtrace.push({ function: err.functionName || '', file: err.fileName || '', line: err.lineNumber || 0, column: err.columnNumber || 0, }); } else { let frames = parse(err); if (frames.length === 0) { try { throw new Error('fake'); } catch (fakeErr) { frames = parse(fakeErr); frames.shift(); frames.shift(); } } for (let frame of frames) { backtrace.push({ function: frame.functionName || '', file: frame.fileName || '', line: frame.lineNumber || 0, column: frame.columnNumber || 0, }); } } let type: string = err.name ? err.name : ''; let msg: string = err.message ? String(err.message) : String(err); return { type, message: msg, backtrace, }; } ================================================ FILE: packages/browser/src/processor/processor.ts ================================================ import { INoticeError } from '../notice'; export type Processor = (err: Error) => INoticeError; ================================================ FILE: packages/browser/src/queries.ts ================================================ import { makeRequester, Requester } from './http_req'; import { IOptions } from './options'; import { hasTdigest, TDigestStat } from './tdshared'; const FLUSH_INTERVAL = 15000; // 15 seconds interface IQueryKey { method: string; route: string; query: string; func: string; file: string; line: number; time: Date; } export class QueryInfo { method = ''; route = ''; query = ''; func = ''; file = ''; line = 0; startTime = new Date(); endTime: Date; constructor(query = '') { this.query = query; } _duration(): number { if (!this.endTime) { this.endTime = new Date(); } return this.endTime.getTime() - this.startTime.getTime(); } } export class QueriesStats { _opt: IOptions; _url: string; _requester: Requester; _m: { [key: string]: TDigestStat } = {}; _timer; constructor(opt: IOptions) { this._opt = opt; this._url = `${opt.host}/api/v5/projects/${opt.projectId}/queries-stats?key=${opt.projectKey}`; this._requester = makeRequester(opt); } start(query = ''): QueryInfo { return new QueryInfo(query); } notify(q: QueryInfo): void { if (!hasTdigest) { return; } if (!this._opt.performanceStats) { return; } if (!this._opt.queryStats) { return; } let ms = q._duration(); const minute = 60 * 1000; let startTime = new Date( Math.floor(q.startTime.getTime() / minute) * minute ); let key: IQueryKey = { method: q.method, route: q.route, query: q.query, func: q.func, file: q.file, line: q.line, time: startTime, }; let keyStr = JSON.stringify(key); let stat = this._m[keyStr]; if (!stat) { stat = new TDigestStat(); this._m[keyStr] = stat; } stat.add(ms); if (this._timer) { return; } this._timer = setTimeout(() => { this._flush(); }, FLUSH_INTERVAL); } _flush(): void { let queries = []; for (let keyStr in this._m) { if (!this._m.hasOwnProperty(keyStr)) { continue; } let key: IQueryKey = JSON.parse(keyStr); let v = { ...key, ...this._m[keyStr].toJSON(), }; queries.push(v); } this._m = {}; this._timer = null; let outJSON = JSON.stringify({ environment: this._opt.environment, queries, }); let req = { method: 'POST', url: this._url, body: outJSON, }; this._requester(req) .then((_resp) => { // nothing }) .catch((err) => { if (console.error) { console.error('can not report queries stats', err); } }); } } ================================================ FILE: packages/browser/src/queues.ts ================================================ import { makeRequester, Requester } from './http_req'; import { BaseMetric } from './metrics'; import { IOptions } from './options'; import { hasTdigest, TDigestStatGroups } from './tdshared'; const FLUSH_INTERVAL = 15000; // 15 seconds interface IQueueKey { queue: string; time: Date; } export class QueueMetric extends BaseMetric { queue: string; constructor(queue: string) { super(); this.queue = queue; this.startTime = new Date(); } } export class QueuesStats { _opt: IOptions; _url: string; _requester: Requester; _m: { [key: string]: TDigestStatGroups } = {}; _timer; constructor(opt: IOptions) { this._opt = opt; this._url = `${opt.host}/api/v5/projects/${opt.projectId}/queues-stats?key=${opt.projectKey}`; this._requester = makeRequester(opt); } notify(q: QueueMetric): void { if (!hasTdigest) { return; } if (!this._opt.performanceStats) { return; } if (!this._opt.queueStats) { return; } let ms = q._duration(); if (ms === 0) { ms = 0.00001; } const minute = 60 * 1000; let startTime = new Date( Math.floor(q.startTime.getTime() / minute) * minute ); let key: IQueueKey = { queue: q.queue, time: startTime, }; let keyStr = JSON.stringify(key); let stat = this._m[keyStr]; if (!stat) { stat = new TDigestStatGroups(); this._m[keyStr] = stat; } stat.addGroups(ms, q._groups); if (this._timer) { return; } this._timer = setTimeout(() => { this._flush(); }, FLUSH_INTERVAL); } _flush(): void { let queues = []; for (let keyStr in this._m) { if (!this._m.hasOwnProperty(keyStr)) { continue; } let key: IQueueKey = JSON.parse(keyStr); let v = { ...key, ...this._m[keyStr].toJSON(), }; queues.push(v); } this._m = {}; this._timer = null; let outJSON = JSON.stringify({ environment: this._opt.environment, queues, }); let req = { method: 'POST', url: this._url, body: outJSON, }; this._requester(req) .then((_resp) => { // nothing }) .catch((err) => { if (console.error) { console.error('can not report queues breakdowns', err); } }); } } ================================================ FILE: packages/browser/src/remote_settings.ts ================================================ import { makeRequester, Requester } from './http_req'; import { IOptions } from './options'; import { NOTIFIER_NAME, NOTIFIER_VERSION } from './version'; // API version to poll. const API_VER = '2020-06-18'; // How frequently we should poll the config API. const DEFAULT_INTERVAL = 600000; // 10 minutes const NOTIFIER_INFO = { notifier_name: NOTIFIER_NAME, notifier_version: NOTIFIER_VERSION, os: typeof window !== 'undefined' && window.navigator && window.navigator.userAgent ? window.navigator.userAgent : undefined, language: 'JavaScript', }; // Remote config settings. const ERROR_SETTING = 'errors'; const APM_SETTING = 'apm'; interface IRemoteConfig { project_id: number; updated_at: number; poll_sec: number; config_route: string; settings: IRemoteConfigSetting[]; } interface IRemoteConfigSetting { name: string; enabled: boolean; endpoint: string; } type Entries = { [K in keyof T]: [K, T[K]]; }[keyof T][]; export class RemoteSettings { _opt: IOptions; _requester: Requester; _data: SettingsData; _origErrorNotifications: boolean; _origPerformanceStats: boolean; constructor(opt: IOptions) { this._opt = opt; this._requester = makeRequester(opt); this._data = new SettingsData(opt.projectId, { project_id: null, poll_sec: 0, updated_at: 0, config_route: '', settings: [], }); this._origErrorNotifications = opt.errorNotifications; this._origPerformanceStats = opt.performanceStats; } poll(): any { // First request is immediate. When it's done, we cancel it since we want to // change interval time to the default value. const pollerId = setInterval(() => { this._doRequest(); clearInterval(pollerId); }, 0); // Second fetch is what always runs in background. return setInterval(this._doRequest.bind(this), DEFAULT_INTERVAL); } _doRequest(): void { this._requester(this._requestParams(this._opt)) .then((resp) => { this._data.merge(resp.json); this._opt.host = this._data.errorHost(); this._opt.apmHost = this._data.apmHost(); this._processErrorNotifications(this._data); this._processPerformanceStats(this._data); }) .catch((_) => { return; }); } _requestParams(opt: IOptions): any { return { method: 'GET', url: this._pollUrl(opt), headers: { Accept: 'application/json', 'Cache-Control': 'no-cache,no-store', }, }; } _pollUrl(opt: IOptions): string { const url = this._data.configRoute(opt.remoteConfigHost); let queryParams = '?'; for (const [key, value] of this._entries(NOTIFIER_INFO)) { queryParams += `&${encodeURIComponent(key)}=${encodeURIComponent(value)}`; } return url + queryParams; } _processErrorNotifications(data: SettingsData): void { if (!this._origErrorNotifications) { return; } this._opt.errorNotifications = data.errorNotifications(); } _processPerformanceStats(data: SettingsData): void { if (!this._origPerformanceStats) { return; } this._opt.performanceStats = data.performanceStats(); } // Polyfill from: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries#polyfill _entries(obj: T): Entries { const ownProps = Object.keys(obj); let i = ownProps.length; const resArray = new Array(i); while (i--) resArray[i] = [ownProps[i], obj[ownProps[i]]]; return resArray; } } export class SettingsData { _projectId: number; _data: IRemoteConfig; constructor(projectId: number, data: IRemoteConfig) { this._projectId = projectId; this._data = data; } merge(other: IRemoteConfig) { this._data = { ...this._data, ...other }; } configRoute(remoteConfigHost: string): string { const host = remoteConfigHost.replace(/\/$/, ''); const configRoute = this._data.config_route; if ( configRoute === null || configRoute === undefined || configRoute === '' ) { return `${host}/${API_VER}/config/${this._projectId}/config.json`; } else { return `${host}/${configRoute}`; } } errorNotifications(): boolean { const s = this._findSetting(ERROR_SETTING); if (s === null) { return true; } return s.enabled; } performanceStats(): boolean { const s = this._findSetting(APM_SETTING); if (s === null) { return true; } return s.enabled; } errorHost(): string { const s = this._findSetting(ERROR_SETTING); if (s === null) { return null; } return s.endpoint; } apmHost(): string { const s = this._findSetting(APM_SETTING); if (s === null) { return null; } return s.endpoint; } _findSetting(name: string): IRemoteConfigSetting { const settings = this._data.settings; if (settings === null || settings === undefined) { return null; } const setting = settings.find((s) => { return s.name === name; }); if (setting === undefined) { return null; } return setting; } } ================================================ FILE: packages/browser/src/routes.ts ================================================ import { makeRequester, Requester } from './http_req'; import { BaseMetric } from './metrics'; import { IOptions } from './options'; import { hasTdigest, TDigestStat, TDigestStatGroups } from './tdshared'; const FLUSH_INTERVAL = 15000; // 15 seconds interface IRouteKey { method: string; route: string; statusCode: number; time: Date; } interface IBreakdownKey { method: string; route: string; responseType: string; time: Date; } export class RouteMetric extends BaseMetric { method: string; route: string; statusCode: number; contentType: string; constructor(method = '', route = '', statusCode = 0, contentType = '') { super(); this.method = method; this.route = route; this.statusCode = statusCode; this.contentType = contentType; this.startTime = new Date(); } } export class RoutesStats { _opt: IOptions; _url: string; _requester: Requester; _m: { [key: string]: TDigestStat } = {}; _timer; constructor(opt: IOptions) { this._opt = opt; this._url = `${opt.host}/api/v5/projects/${opt.projectId}/routes-stats?key=${opt.projectKey}`; this._requester = makeRequester(opt); } notify(req: RouteMetric): void { if (!hasTdigest) { return; } if (!this._opt.performanceStats) { return; } let ms = req._duration(); const minute = 60 * 1000; let startTime = new Date( Math.floor(req.startTime.getTime() / minute) * minute ); let key: IRouteKey = { method: req.method, route: req.route, statusCode: req.statusCode, time: startTime, }; let keyStr = JSON.stringify(key); let stat = this._m[keyStr]; if (!stat) { stat = new TDigestStat(); this._m[keyStr] = stat; } stat.add(ms); if (this._timer) { return; } this._timer = setTimeout(() => { this._flush(); }, FLUSH_INTERVAL); } _flush(): void { let routes = []; for (let keyStr in this._m) { if (!this._m.hasOwnProperty(keyStr)) { continue; } let key: IRouteKey = JSON.parse(keyStr); let v = { ...key, ...this._m[keyStr].toJSON(), }; routes.push(v); } this._m = {}; this._timer = null; let outJSON = JSON.stringify({ environment: this._opt.environment, routes, }); let req = { method: 'POST', url: this._url, body: outJSON, }; this._requester(req) .then((_resp) => { // nothing }) .catch((err) => { if (console.error) { console.error('can not report routes stats', err); } }); } } export class RoutesBreakdowns { _opt: IOptions; _url: string; _requester: Requester; _m: { [key: string]: TDigestStatGroups } = {}; _timer; constructor(opt: IOptions) { this._opt = opt; this._url = `${opt.host}/api/v5/projects/${opt.projectId}/routes-breakdowns?key=${opt.projectKey}`; this._requester = makeRequester(opt); } notify(req: RouteMetric): void { if (!hasTdigest) { return; } if (!this._opt.performanceStats) { return; } if ( req.statusCode < 200 || (req.statusCode >= 300 && req.statusCode < 400) || req.statusCode === 404 || Object.keys(req._groups).length === 0 ) { return; } let ms = req._duration(); if (ms === 0) { ms = 0.00001; } const minute = 60 * 1000; let startTime = new Date( Math.floor(req.startTime.getTime() / minute) * minute ); let key: IBreakdownKey = { method: req.method, route: req.route, responseType: this._responseType(req), time: startTime, }; let keyStr = JSON.stringify(key); let stat = this._m[keyStr]; if (!stat) { stat = new TDigestStatGroups(); this._m[keyStr] = stat; } stat.addGroups(ms, req._groups); if (this._timer) { return; } this._timer = setTimeout(() => { this._flush(); }, FLUSH_INTERVAL); } _flush(): void { let routes = []; for (let keyStr in this._m) { if (!this._m.hasOwnProperty(keyStr)) { continue; } let key: IBreakdownKey = JSON.parse(keyStr); let v = { ...key, ...this._m[keyStr].toJSON(), }; routes.push(v); } this._m = {}; this._timer = null; let outJSON = JSON.stringify({ environment: this._opt.environment, routes, }); let req = { method: 'POST', url: this._url, body: outJSON, }; this._requester(req) .then((_resp) => { // nothing }) .catch((err) => { if (console.error) { console.error('can not report routes breakdowns', err); } }); } _responseType(req: RouteMetric): string { if (req.statusCode >= 500) { return '5xx'; } if (req.statusCode >= 400) { return '4xx'; } if (!req.contentType) { return ''; } const s = req.contentType.split(';')[0].split('/'); return s[s.length - 1]; } } ================================================ FILE: packages/browser/src/scope.ts ================================================ import { IMetric, NoopMetric } from './metrics'; interface IHistoryRecord { type: string; date?: Date; [key: string]: any; } interface IMap { [key: string]: any; } export class Scope { _noopMetric = new NoopMetric(); _routeMetric: IMetric; _queueMetric: IMetric; _context: IMap = {}; _historyMaxLen = 20; _history: IHistoryRecord[] = []; _lastRecord: IHistoryRecord; clone(): Scope { const clone = new Scope(); clone._context = { ...this._context }; clone._history = this._history.slice(); return clone; } setContext(context: IMap) { this._context = { ...this._context, ...context }; } context(): IMap { const ctx = { ...this._context }; if (this._history.length > 0) { ctx.history = this._history.slice(); } return ctx; } pushHistory(state: IHistoryRecord): void { if (this._isDupState(state)) { if (this._lastRecord.num) { this._lastRecord.num++; } else { this._lastRecord.num = 2; } return; } if (!state.date) { state.date = new Date(); } this._history.push(state); this._lastRecord = state; if (this._history.length > this._historyMaxLen) { this._history = this._history.slice(-this._historyMaxLen); } } private _isDupState(state): boolean { if (!this._lastRecord) { return false; } for (let key in state) { if (!state.hasOwnProperty(key) || key === 'date') { continue; } if (state[key] !== this._lastRecord[key]) { return false; } } return true; } routeMetric(): IMetric { return this._routeMetric || this._noopMetric; } setRouteMetric(metric: IMetric) { this._routeMetric = metric; } queueMetric(): IMetric { return this._queueMetric || this._noopMetric; } setQueueMetric(metric: IMetric) { this._queueMetric = metric; } } ================================================ FILE: packages/browser/src/tdshared.ts ================================================ let tdigest; export let hasTdigest = false; try { tdigest = require('tdigest'); hasTdigest = true; } catch (err) {} interface ICentroid { mean: number; n: number; } interface ICentroids { each(fn: (c: ICentroid) => void): void; } interface ITDigest { centroids: ICentroids; push(x: number); compress(); } interface ITDigestCentroids { mean: number[]; count: number[]; } export class TDigestStat { count = 0; sum = 0; sumsq = 0; _td = new tdigest.Digest(); add(ms: number) { if (ms === 0) { ms = 0.00001; } this.count += 1; this.sum += ms; this.sumsq += ms * ms; if (this._td) { this._td.push(ms); } } toJSON() { return { count: this.count, sum: this.sum, sumsq: this.sumsq, tdigestCentroids: tdigestCentroids(this._td), }; } } export class TDigestStatGroups extends TDigestStat { groups: { [key: string]: TDigestStat } = {}; addGroups(totalMs: number, groups: { [key: string]: number }) { this.add(totalMs); for (const name in groups) { if (groups.hasOwnProperty(name)) { this.addGroup(name, groups[name]); } } } addGroup(name: string, ms: number) { let stat = this.groups[name]; if (!stat) { stat = new TDigestStat(); this.groups[name] = stat; } stat.add(ms); } toJSON() { return { count: this.count, sum: this.sum, sumsq: this.sumsq, tdigestCentroids: tdigestCentroids(this._td), groups: this.groups, }; } } function tdigestCentroids(td: ITDigest): ITDigestCentroids { let means: number[] = []; let counts: number[] = []; td.centroids.each((c: ICentroid) => { means.push(c.mean); counts.push(c.n); }); return { mean: means, count: counts, }; } ================================================ FILE: packages/browser/src/version.ts ================================================ export const NOTIFIER_NAME = 'airbrake-js/browser'; export const NOTIFIER_VERSION = '2.1.9'; export const NOTIFIER_URL = 'https://github.com/airbrake/airbrake-js/tree/master/packages/browser'; ================================================ FILE: packages/browser/tests/client.test.js ================================================ import { Notifier } from '../src/notifier'; describe('Notifier config', () => { const reporter = jest.fn(() => Promise.resolve({ errors: [] })); const err = new Error('test'); let client; test('throws when projectId or projectKey are missing', () => { expect(() => { new Notifier({}); }).toThrow('airbrake: projectId and projectKey are required'); }); test('calls a reporter', () => { client = new Notifier({ projectId: 1, projectKey: 'abc', reporter, remoteConfig: false, }); client.notify(err); expect(reporter.mock.calls.length).toBe(1); }); test('supports environment', () => { client = new Notifier({ projectId: 1, projectKey: 'abc', reporter, environment: 'production', remoteConfig: false, }); client.notify(err); expect(reporter.mock.calls.length).toBe(1); let notice = reporter.mock.calls[0][0]; expect(notice.context.environment).toBe('production'); }); describe('keysBlocklist', () => { function test(keysBlocklist) { client = new Notifier({ projectId: 1, projectKey: 'abc', reporter, keysBlocklist, remoteConfig: false, }); client.notify({ error: err, params: { key1: 'value1', key2: 'value2', key3: { key1: 'value1' }, }, }); expect(reporter.mock.calls.length).toBe(1); let notice = reporter.mock.calls[0][0]; expect(notice.params).toStrictEqual({ key1: '[Filtered]', key2: 'value2', key3: { key1: '[Filtered]' }, }); } it('supports exact match', () => { test(['key1']); }); it('supports regexp match', () => { test([/key1/]); }); }); }); describe('Notifier', () => { let reporter; let client; let theErr = new Error('test'); beforeEach(() => { reporter = jest.fn(() => { return Promise.resolve({ id: 1 }); }); client = new Notifier({ projectId: 1, projectKey: 'abc', reporter, remoteConfig: false, }); }); describe('filter', () => { it('returns null to ignore notice', () => { let filter = jest.fn((_) => null); client.addFilter(filter); client.notify({}); expect(filter.mock.calls.length).toBe(1); expect(reporter.mock.calls.length).toBe(0); }); it('returns notice to keep it', () => { let filter = jest.fn((notice) => notice); client.addFilter(filter); client.notify({}); expect(filter.mock.calls.length).toBe(1); expect(reporter.mock.calls.length).toBe(1); }); it('returns notice to change payload', () => { let filter = jest.fn((notice) => { notice.context.environment = 'production'; return notice; }); client.addFilter(filter); client.notify({}); expect(filter.mock.calls.length).toBe(1); let notice = reporter.mock.calls[0][0]; expect(notice.context.environment).toBe('production'); }); it('returns new notice to change payload', () => { let newNotice = { errors: [] }; let filter = jest.fn((_) => { return newNotice; }); client.addFilter(filter); client.notify({}); expect(filter.mock.calls.length).toBe(1); let notice = reporter.mock.calls[0][0]; expect(notice).toEqual(newNotice); }); }); describe('"Uncaught ..." error message', () => { beforeEach(() => { let msg = 'Uncaught SecurityError: Blocked a frame with origin "https://airbrake.io" from accessing a cross-origin frame.'; client.notify({ type: '', message: msg }); }); it('splitted into type and message', () => { expect(reporter.mock.calls.length).toBe(1); let notice = reporter.mock.calls[0][0]; let err = notice.errors[0]; expect(err.type).toBe('SecurityError'); expect(err.message).toBe( 'Blocked a frame with origin "https://airbrake.io" from accessing a cross-origin frame.' ); }); }); describe('Angular error message', () => { beforeEach(() => { let msg = `[$injector:undef] Provider '$exceptionHandler' must return a value from $get factory method.\nhttp://errors.angularjs.org/1.4.3/$injector/undef?p0=%24exceptionHandler`; client.notify({ type: 'Error', message: msg }); }); it('splitted into type and message', () => { expect(reporter.mock.calls.length).toBe(1); let notice = reporter.mock.calls[0][0]; let err = notice.errors[0]; expect(err.type).toBe('$injector:undef'); expect(err.message).toBe( `Provider '$exceptionHandler' must return a value from $get factory method.\nhttp://errors.angularjs.org/1.4.3/$injector/undef?p0=%24exceptionHandler` ); }); }); describe('severity', () => { it('defaults to "error"', () => { client.notify(theErr); let reported = reporter.mock.calls[0][0]; expect(reported.context.severity).toBe('error'); }); it('can be overriden', () => { let customSeverity = 'emergency'; client.addFilter((n) => { n.context.severity = customSeverity; return n; }); client.notify(theErr); let reported = reporter.mock.calls[0][0]; expect(reported.context.severity).toBe(customSeverity); }); }); describe('notify', () => { it('calls reporter', () => { client.notify(theErr); expect(reporter.mock.calls.length).toBe(1); }); describe('when errorNotifications is disabled', () => { beforeEach(() => { client = new Notifier({ projectId: 1, projectKey: 'abc', reporter, environment: 'production', errorNotifications: false, remoteConfig: false, }); }); it('does not call reporter', () => { client.notify(theErr); expect(reporter.mock.calls.length).toBe(0); }); it('returns promise and resolves it', (done) => { let promise = client.notify(theErr); let onResolved = jest.fn(); promise.then(onResolved); setTimeout(() => { expect(onResolved.mock.calls.length).toBe(1); done(); }, 0); }); }); it('returns promise and resolves it', (done) => { let promise = client.notify(theErr); let onResolved = jest.fn(); promise.then(onResolved); setTimeout(() => { expect(onResolved.mock.calls.length).toBe(1); done(); }, 0); }); it('does not report same error twice', (done) => { client.notify(theErr); expect(reporter.mock.calls.length).toBe(1); let promise = client.notify(theErr); promise.then((notice) => { expect(notice.error.toString()).toBe( 'Error: airbrake: error is filtered' ); done(); }); }); it('reports NaN errors', () => { client.notify(NaN); expect(reporter.mock.calls.length).toEqual(1); let notice = reporter.mock.calls[0][0]; expect(notice.errors[0].message).toEqual('NaN'); }); it('reports undefined errors', () => { client.notify(undefined); expect(reporter.mock.calls.length).toEqual(1); let notice = reporter.mock.calls[0][0]; expect(notice.errors[0].message).toEqual('undefined'); }); it('reports empty string errors', () => { client.notify(''); expect(reporter.mock.calls.length).toEqual(1); let notice = reporter.mock.calls[0][0]; expect(notice.errors[0].message).toEqual(''); }); it('reports "false"', () => { client.notify(false); expect(reporter.mock.calls.length).toEqual(1); let notice = reporter.mock.calls[0][0]; expect(notice.errors[0].message).toEqual('false'); }); it('reports "null"', () => { client.notify(null); expect(reporter.mock.calls.length).toEqual(1); let notice = reporter.mock.calls[0][0]; expect(notice.errors[0].message).toEqual('null'); }); it('reports severity', () => { client.notify({ error: theErr, context: { severity: 'warning' } }); let notice = reporter.mock.calls[0][0]; expect(notice.context.severity).toBe('warning'); }); it('reports userAgent', () => { client.notify(theErr); let notice = reporter.mock.calls[0][0]; expect(notice.context.userAgent).toContain('Mozilla'); }); it('reports text error', () => { client.notify('hello'); expect(reporter.mock.calls.length).toBe(1); let notice = reporter.mock.calls[0][0]; let err = notice.errors[0]; expect(err.message).toBe('hello'); expect(err.backtrace.length).not.toBe(0); }); it('ignores "Script error" message', () => { client.notify('Script error'); expect(reporter.mock.calls.length).toBe(0); }); it('ignores "InvalidAccessError" message', () => { client.notify('InvalidAccessError'); expect(reporter.mock.calls.length).toBe(0); }); it('ignores errors occurred in file', () => { client.notify({ message: 'test', fileName: '' }); expect(reporter.mock.calls.length).toBe(0); }); describe('custom data in the filter', () => { it('reports context', () => { client.addFilter((n) => { n.context.context_key = '[custom_context]'; return n; }); client.notify(theErr); let reported = reporter.mock.calls[0][0]; expect(reported.context.context_key).toEqual('[custom_context]'); }); it('reports environment', () => { client.addFilter((n) => { n.environment.env_key = '[custom_env]'; return n; }); client.notify(theErr); let reported = reporter.mock.calls[0][0]; expect(reported.environment.env_key).toEqual('[custom_env]'); }); it('reports params', () => { client.addFilter((n) => { n.params.params_key = '[custom_params]'; return n; }); client.notify(theErr); let reported = reporter.mock.calls[0][0]; expect(reported.params.params_key).toEqual('[custom_params]'); }); it('reports session', () => { client.addFilter((n) => { n.session.session_key = '[custom_session]'; return n; }); client.notify(theErr); let reported = reporter.mock.calls[0][0]; expect(reported.session.session_key).toEqual('[custom_session]'); }); }); describe('wrapped error', () => { it('unwraps and processes error', () => { client.notify({ error: theErr }); expect(reporter.mock.calls.length).toBe(1); }); it('reports NaN errors', () => { client.notify({ error: NaN }); expect(reporter.mock.calls.length).toEqual(1); let notice = reporter.mock.calls[0][0]; expect(notice.errors[0].message).toEqual('NaN'); }); it('reports undefined errors', () => { client.notify({ error: undefined }); expect(reporter.mock.calls.length).toEqual(1); let notice = reporter.mock.calls[0][0]; expect(notice.errors[0].message).toEqual('undefined'); }); it('reports empty string errors', () => { client.notify({ error: '' }); expect(reporter.mock.calls.length).toEqual(1); let notice = reporter.mock.calls[0][0]; expect(notice.errors[0].message).toEqual(''); }); it('reports "false"', () => { client.notify({ error: false }); expect(reporter.mock.calls.length).toEqual(1); let notice = reporter.mock.calls[0][0]; expect(notice.errors[0].message).toEqual('false'); }); it('reports "null"', () => { client.notify({ error: null }); expect(reporter.mock.calls.length).toEqual(1); let notice = reporter.mock.calls[0][0]; expect(notice.errors[0].message).toEqual('null'); }); it('reports custom context', () => { client.addFilter((n) => { n.context.context1 = 'value1'; n.context.context2 = 'value2'; return n; }); client.notify({ error: theErr, context: { context1: 'notify_value1', context3: 'notify_value3', }, }); let reported = reporter.mock.calls[0][0]; expect(reported.context.context1).toBe('value1'); expect(reported.context.context2).toBe('value2'); expect(reported.context.context3).toBe('notify_value3'); }); it('reports custom environment', () => { client.addFilter((n) => { n.environment.env1 = 'value1'; n.environment.env2 = 'value2'; return n; }); client.notify({ error: theErr, environment: { env1: 'notify_value1', env3: 'notify_value3', }, }); let reported = reporter.mock.calls[0][0]; expect(reported.environment).toStrictEqual({ env1: 'value1', env2: 'value2', env3: 'notify_value3', }); }); it('reports custom params', () => { client.addFilter((n) => { n.params.param1 = 'value1'; n.params.param2 = 'value2'; return n; }); client.notify({ error: theErr, params: { param1: 'notify_value1', param3: 'notify_value3', }, }); let params = reporter.mock.calls[0][0].params; expect(params.param1).toBe('value1'); expect(params.param2).toBe('value2'); expect(params.param3).toBe('notify_value3'); }); it('reports custom session', () => { client.addFilter((n) => { n.session.session1 = 'value1'; n.session.session2 = 'value2'; return n; }); client.notify({ error: theErr, session: { session1: 'notify_value1', session3: 'notify_value3', }, }); let reported = reporter.mock.calls[0][0]; expect(reported.session).toStrictEqual({ session1: 'value1', session2: 'value2', session3: 'notify_value3', }); }); }); }); describe('location', () => { let notice; beforeEach(() => { client.notify(theErr); expect(reporter.mock.calls.length).toBe(1); notice = reporter.mock.calls[0][0]; }); it('reports context.url', () => { expect(notice.context.url).toEqual('http://localhost/'); }); it('reports context.rootDirectory', () => { expect(notice.context.rootDirectory).toEqual('http://localhost'); }); }); describe('wrap', () => { it('does not invoke function immediately', () => { let fn = jest.fn(); client.wrap(fn); expect(fn.mock.calls.length).toBe(0); }); it('creates wrapper that invokes function with passed args', () => { let fn = jest.fn(); let wrapper = client.wrap(fn); wrapper('hello', 'world'); expect(fn.mock.calls.length).toBe(1); expect(fn.mock.calls[0]).toEqual(['hello', 'world']); }); it('sets _airbrake and inner properties', () => { let fn = jest.fn(); let wrapper = client.wrap(fn); expect(wrapper._airbrake).toEqual(true); expect(wrapper.inner).toEqual(fn); }); it('copies function properties', () => { let fn = jest.fn(); fn.prop = 'hello'; let wrapper = client.wrap(fn); expect(wrapper.prop).toEqual('hello'); }); it('reports throwed exception', () => { let spy = jest.fn(); client.notify = spy; let fn = () => { throw theErr; }; let wrapper = client.wrap(fn); try { wrapper('hello', 'world'); } catch (_) { // ignore } expect(spy.mock.calls.length).toBe(1); expect(spy.mock.calls[0]).toEqual([ { error: theErr, params: { arguments: ['hello', 'world'] }, }, ]); }); it('wraps arguments', () => { let fn = jest.fn(); let wrapper = client.wrap(fn); let arg1 = () => null; wrapper(arg1); expect(fn.mock.calls.length).toBe(1); let arg1Wrapper = fn.mock.calls[0][0]; expect(arg1Wrapper._airbrake).toEqual(true); expect(arg1Wrapper.inner).toEqual(arg1); }); }); describe('call', () => { it('reports throwed exception', () => { let spy = jest.fn(); client.notify = spy; let fn = () => { throw theErr; }; try { client.call(fn, 'hello', 'world'); } catch (_) { // ignore } expect(spy.mock.calls.length).toBe(1); expect(spy.mock.calls[0]).toEqual([ { error: theErr, params: { arguments: ['hello', 'world'] }, }, ]); }); }); describe('offline', () => { let spy; beforeEach(() => { let event = new Event('offline'); window.dispatchEvent(event); let promise = client.notify(theErr); spy = jest.fn(); promise.then(spy); }); it('causes client to not report errors', () => { expect(reporter.mock.calls.length).toBe(0); }); describe('online', () => { beforeEach(() => { let event = new Event('online'); window.dispatchEvent(event); }); it('causes client to report queued errors', () => { expect(reporter.mock.calls.length).toBe(1); }); it('resolves promise', (done) => { setTimeout(() => { expect(spy.mock.calls.length).toBe(1); done(); }, 0); }); }); }); describe('errorNotifications', () => { it('is set to true by default when it is not specified', () => { client = new Notifier({ projectId: 1, projectKey: 'abc', remoteConfig: false, }); expect(client._opt.errorNotifications).toBe(true); }); }); describe('performanceStats', () => { it('is set to true by default when it is not specified', () => { client = new Notifier({ projectId: 1, projectKey: 'abc', remoteConfig: false, }); expect(client._opt.performanceStats).toBe(true); }); }); describe('queryStats', () => { it('is set to true by default when it is not specified', () => { client = new Notifier({ projectId: 1, projectKey: 'abc', remoteConfig: false, }); expect(client._opt.queryStats).toBe(true); }); }); describe('queueStats', () => { it('is set to true by default when it is not specified', () => { client = new Notifier({ projectId: 1, projectKey: 'abc', remoteConfig: false, }); expect(client._opt.queueStats).toBe(true); }); }); }); ================================================ FILE: packages/browser/tests/historian.test.js ================================================ let { fetch, Request } = require('cross-fetch'); window.fetch = fetch; import { Notifier } from '../src/notifier'; class Location { constructor(s) { this.s = s; } toString() { return this.s; } } describe('instrumentation', () => { let processor; let reporter; let client; beforeEach(() => { processor = jest.fn((data) => { return data; }); reporter = jest.fn(() => { return Promise.resolve({ id: 1 }); }); jest .spyOn(global.console, 'log') .mockImplementation((args) => Promise.resolve(args)); client = new Notifier({ projectId: 1, projectKey: 'abc', processor, reporter, remoteConfig: false, }); }); describe('location', () => { beforeEach(() => { let locations = ['', 'http://hello/world', 'foo', new Location('/')]; for (let loc of locations) { try { window.history.pushState(null, '', loc); } catch (_) { // ignore } } client.notify(new Error('test')); }); it('records browser history', () => { expect(reporter.mock.calls.length).toBe(1); let notice = reporter.mock.calls[0][0]; let history = notice.context.history; let state = history[history.length - 3]; delete state.date; expect(state).toStrictEqual({ type: 'location', from: '/', to: '/world', }); state = history[history.length - 2]; delete state.date; expect(state).toStrictEqual({ type: 'location', from: '/world', to: '/foo', }); state = history[history.length - 1]; delete state.date; expect(state).toStrictEqual({ type: 'location', from: '/foo', to: '/', }); }); }); describe('XHR', () => { // TODO: use a mock instead of actually sending http requests beforeEach(() => { let promise = new Promise((resolve, reject) => { var req = new XMLHttpRequest(); req.open('GET', 'https://httpbin.org/get'); req.onreadystatechange = () => { if (req.readyState != 4) return; if (req.status == 200) { resolve(req.response); } else { reject(); } }; req.send(); }); promise.then(() => { client.notify(new Error('test')); }); return promise; }); it('records request', () => { expect(reporter.mock.calls.length).toBe(1); let notice = reporter.mock.calls[0][0]; let history = notice.context.history; let state = history[history.length - 1]; expect(state.type).toBe('xhr'); expect(state.method).toBe('GET'); expect(state.url).toBe('https://httpbin.org/get'); expect(state.statusCode).toBe(200); expect(state.duration).toEqual(expect.any(Number)); }); }); describe('fetch', () => { // TODO: use a mock instead of actually sending http requests describe('simple fetch', () => { beforeEach(() => { let promise = window.fetch('https://httpbin.org/get'); promise.then(() => { client.notify(new Error('test')); }); return promise; }); it('records request', () => { expect(reporter.mock.calls.length).toBe(1); let notice = reporter.mock.calls[0][0]; let history = notice.context.history; let state = history[history.length - 1]; expect(state.type).toBe('xhr'); expect(state.method).toBe('GET'); expect(state.url).toBe('https://httpbin.org/get'); expect(state.statusCode).toBe(200); expect(state.duration).toEqual(expect.any(Number)); }); }); describe('fetch with options', () => { beforeEach(() => { let promise = window.fetch('https://httpbin.org/post', { method: 'POST', }); promise.then(() => { client.notify(new Error('test')); }); return promise; }); it('records request', () => { expect(reporter.mock.calls.length).toBe(1); let notice = reporter.mock.calls[0][0]; let history = notice.context.history; let state = history[history.length - 1]; expect(state.type).toBe('xhr'); expect(state.method).toBe('POST'); expect(state.url).toBe('https://httpbin.org/post'); expect(state.statusCode).toBe(200); expect(state.duration).toEqual(expect.any(Number)); }); }); describe('fetch with Request object', () => { beforeEach(() => { const req = new Request('https://httpbin.org/post', { method: 'POST', body: '{"foo": "bar"}', }); let promise = window.fetch(req); promise.then(() => { client.notify(new Error('test')); }); return promise; }); it('records request', () => { expect(reporter.mock.calls.length).toBe(1); let notice = reporter.mock.calls[0][0]; let history = notice.context.history; let state = history[history.length - 1]; expect(state.type).toBe('xhr'); expect(state.method).toBe('POST'); expect(state.url).toBe('https://httpbin.org/post'); expect(state.statusCode).toBe(200); expect(state.duration).toEqual(expect.any(Number)); }); }); }); describe('console', () => { beforeEach(() => { for (let i = 0; i < 25; i++) { // tslint:disable-next-line:no-console console.log(i); } client.notify(new Error('test')); }); it('records log message', () => { expect(reporter.mock.calls.length).toBe(1); let notice = reporter.mock.calls[0][0]; let history = notice.context.history; expect(history).toHaveLength(20); for (let i in history) { if (!history.hasOwnProperty(i)) { continue; } let state = history[i]; expect(state.type).toBe('log'); expect(state.severity).toBe('log'); expect(state.arguments).toStrictEqual([+i + 5]); expect(state.date).not.toBeNull(); } }); }); }); ================================================ FILE: packages/browser/tests/jsonify_notice.test.js ================================================ import { jsonifyNotice } from '../src/jsonify_notice'; describe('jsonify_notice', () => { const maxLength = 30000; describe('when called with notice', () => { let notice = { params: { arguments: [] }, environment: { env1: 'value1' }, session: { session1: 'value1' }, }; let json; beforeEach(() => { json = jsonifyNotice(notice); }); it('produces valid JSON', () => { expect(JSON.parse(json)).toStrictEqual(notice); }); }); describe('when called with huge notice', () => { let json; beforeEach(() => { let notice = { params: { arr: [] }, }; for (let i = 0; i < 100; i++) { notice.params.arr.push(Array(100).join('x')); } json = jsonifyNotice(notice, { maxLength }); }); it('limits json size', () => { expect(json.length).toBeLessThan(maxLength); }); }); describe('when called with one huge string', () => { let json; beforeEach(() => { let notice = { params: { str: Array(100000).join('x') }, }; json = jsonifyNotice(notice, { maxLength }); }); it('limits json size', () => { expect(json.length).toBeLessThan(maxLength); }); }); describe('when called with huge error message', () => { let json; beforeEach(() => { let notice = { errors: [ { type: Array(100000).join('x'), message: Array(100000).join('x'), }, ], }; json = jsonifyNotice(notice, { maxLength }); }); it('limits json size', () => { expect(json.length).toBeLessThan(maxLength); }); }); describe('when called with huger array', () => { let json; beforeEach(() => { let notice = { params: { param1: Array(100000) }, }; json = jsonifyNotice(notice, { maxLength }); }); it('limits json size', () => { expect(json.length).toBeLessThan(maxLength); }); }); describe('when called with a blocklisted key', () => { const notice = { params: { name: 'I will be filtered' }, session: { session1: 'value1' }, context: { notifier: { name: 'airbrake-js' } }, }; let json; beforeEach(() => { json = jsonifyNotice(notice, { keysBlocklist: ['name'] }); }); it('filters out blocklisted keys', () => { expect(JSON.parse(json)).toStrictEqual({ params: { name: '[Filtered]' }, session: { session1: 'value1' }, context: { notifier: { name: 'airbrake-js' } }, }); }); }); describe('keysAllowlist', () => { describe('when the allowlist key is a string', () => { const notice = { params: { name: 'I am allowlisted', email: 'I will be filtered' }, session: { session1: 'I will be filtered, too' }, context: { notifier: { name: 'I am allowlisted' } }, }; let json; beforeEach(() => { json = jsonifyNotice(notice, { keysAllowlist: ['name'] }); }); it('filters out everything but allowlisted keys', () => { expect(JSON.parse(json)).toStrictEqual({ params: { name: 'I am allowlisted', email: '[Filtered]' }, session: { session1: '[Filtered]' }, context: { notifier: { name: 'I am allowlisted' } }, }); }); }); describe('when the allowlist key is a regexp', () => { const notice = { params: { name: 'I am allowlisted', email: 'I will be filtered' }, session: { session1: 'I will be filtered, too' }, context: { notifier: { name: 'I am allowlisted' } }, }; let json; beforeEach(() => { json = jsonifyNotice(notice, { keysAllowlist: [/nam/] }); }); it('filters out everything but allowlisted keys', () => { expect(JSON.parse(json)).toStrictEqual({ params: { name: 'I am allowlisted', email: '[Filtered]' }, session: { session1: '[Filtered]' }, context: { notifier: { name: 'I am allowlisted' } }, }); }); }); }); describe('when called both with a blocklist and an allowlist', () => { const notice = { params: { name: 'Name' }, session: { session1: 'value1' }, context: { notifier: { name: 'airbrake-js' } }, }; let json; beforeEach(() => { json = jsonifyNotice(notice, { keysBlocklist: ['name'], keysAllowlist: ['name'], }); }); it('ignores the blocklist and uses the allowlist', () => { expect(JSON.parse(json)).toStrictEqual({ params: { name: 'Name' }, session: { session1: '[Filtered]' }, context: { notifier: { name: 'airbrake-js' } }, }); }); }); }); ================================================ FILE: packages/browser/tests/processor/stacktracejs.test.js ================================================ import { INoticeError } from '../../src/notice'; import { espProcessor } from '../../src/processor/esp'; describe('stacktracejs processor', () => { let error; describe('Error', () => { function throwTestError() { try { throw new Error('BOOM'); } catch (err) { error = espProcessor(err); } } beforeEach(() => { throwTestError(); }); it('provides type and message', () => { expect(error.type).toBe('Error'); expect(error.message).toBe('BOOM'); }); it('provides backtrace', () => { let backtrace = error.backtrace; expect(backtrace.length).toBeGreaterThanOrEqual(5); let frame = backtrace[0]; expect(frame.file).toContain('tests/processor/stacktracejs.test'); expect(frame.function).toBe('throwTestError'); expect(frame.line).toEqual(expect.any(Number)); expect(frame.column).toEqual(expect.any(Number)); }); }); describe('text', () => { beforeEach(() => { let err; err = 'BOOM'; error = espProcessor(err); }); it('uses text as error message', () => { expect(error.type).toBe(''); expect(error.message).toBe('BOOM'); }); it('provides backtrace', () => { let backtrace = error.backtrace; expect(backtrace.length).toBeGreaterThanOrEqual(4); }); }); }); ================================================ FILE: packages/browser/tests/remote_settings.test.js ================================================ import { SettingsData } from '../src/remote_settings'; describe('SettingsData', () => { describe('merge', () => { it('merges JSON with a SettingsData', () => { const disabledApm = { settings: [{ name: 'apm', enabled: false }] }; const enabledApm = { settings: [{ name: 'apm', enabled: true }] }; const s = new SettingsData(1, disabledApm); s.merge(enabledApm); expect(s._data).toMatchObject(enabledApm); }); }); describe('configRoute', () => { describe('when config_route in JSON is null', () => { it('returns the default route', () => { const s = new SettingsData(1, { config_route: null }); expect(s.configRoute('http://example.com/')).toMatch( 'http://example.com/2020-06-18/config/1/config.json' ); }); }); describe('when config_route in JSON is undefined', () => { it('returns the default route', () => { const s = new SettingsData(1, { config_route: undefined }); expect(s.configRoute('http://example.com/')).toMatch( 'http://example.com/2020-06-18/config/1/config.json' ); }); }); describe('when config_route in JSON is an empty string', () => { it('returns the default route', () => { const s = new SettingsData(1, { config_route: '' }); expect(s.configRoute('http://example.com/')).toMatch( 'http://example.com/2020-06-18/config/1/config.json' ); }); }); describe('when config_route in JSON is specified', () => { it('returns the specified route', () => { const s = new SettingsData(1, { config_route: 'ROUTE/cfg.json' }); expect(s.configRoute('http://example.com/')).toMatch( 'http://example.com/ROUTE/cfg.json' ); }); }); describe('when the given host does not contain an ending slash', () => { it('returns the specified route', () => { const s = new SettingsData(1, { config_route: 'ROUTE/cfg.json' }); expect(s.configRoute('http://example.com')).toMatch( 'http://example.com/ROUTE/cfg.json' ); }); }); }); describe('errorNotifications', () => { describe('when the "errors" setting exists', () => { describe('and when it is enabled', () => { it('returns true', () => { const s = new SettingsData(1, { settings: [{ name: 'errors', enabled: true }], }); expect(s.errorNotifications()).toBe(true); }); }); describe('and when it is disabled', () => { it('returns false', () => { const s = new SettingsData(1, { settings: [{ name: 'errors', enabled: false }], }); expect(s.errorNotifications()).toBe(false); }); }); }); describe('when the "errors" setting DOES NOT exist', () => { it('returns true', () => { const s = new SettingsData(1, {}); expect(s.errorNotifications()).toBe(true); }); }); }); describe('performanceStats', () => { describe('when the "apm" setting exists', () => { describe('and when it is enabled', () => { it('returns true', () => { const s = new SettingsData(1, { settings: [{ name: 'apm', enabled: true }], }); expect(s.performanceStats()).toBe(true); }); }); describe('and when it is disabled', () => { it('returns false', () => { const s = new SettingsData(1, { settings: [{ name: 'apm', enabled: false }], }); expect(s.performanceStats()).toBe(false); }); }); }); describe('when the "errors" setting DOES NOT exist', () => { it('returns true', () => { const s = new SettingsData(1, {}); expect(s.performanceStats()).toBe(true); }); }); }); describe('errorHost', () => { describe('when the "errors" setting exists', () => { describe('and when it has an endpoint specified', () => { it('returns the endpoint', () => { const s = new SettingsData(1, { settings: [{ name: 'errors', endpoint: 'http://example.com' }], }); expect(s.errorHost()).toMatch('http://example.com'); }); }); describe('and when it has null endpoint', () => { it('returns null', () => { const s = new SettingsData(1, { settings: [{ name: 'errors', endpoint: null }], }); expect(s.errorHost()).toBe(null); }); }); }); describe('when the "errors" setting DOES NOT exist', () => { it('returns null', () => { const s = new SettingsData(1, {}); expect(s.errorHost()).toBe(null); }); }); }); describe('apmHost', () => { describe('when the "apm" setting exists', () => { describe('and when it has an endpoint specified', () => { it('returns the endpoint', () => { const s = new SettingsData(1, { settings: [{ name: 'apm', endpoint: 'http://example.com' }], }); expect(s.apmHost()).toMatch('http://example.com'); }); }); describe('and when it has null endpoint', () => { it('returns null', () => { const s = new SettingsData(1, { settings: [{ name: 'apm', endpoint: null }], }); expect(s.apmHost()).toBe(null); }); }); }); describe('when the "apm" setting DOES NOT exist', () => { it('returns null', () => { const s = new SettingsData(1, {}); expect(s.apmHost()).toBe(null); }); }); }); }); ================================================ FILE: packages/browser/tests/truncate.test.js ================================================ import { truncate } from '../src/jsonify_notice'; describe('truncate', () => { it('works', () => { /* tslint:disable */ let tests = [ [undefined], [null], [true], [false], [new Boolean(true)], [1], [3.14], [new Number(1)], [Infinity], [NaN], [Math.LN2], ['hello'], [new String('hello'), 'hello'], [['foo', 'bar']], [{ foo: 'bar' }], [new Date()], [/a/], [new RegExp('a')], [new Error('hello'), 'Error: hello'], ]; /* tslint:enable */ for (let test of tests) { let wanted = test.length >= 2 ? test[1] : test[0]; if (isNaN(wanted)) { continue; } expect(truncate(test[0])).toBe(wanted); } }); it('omits functions in object', () => { /* tslint:disable */ let obj = { foo: 'bar', fn1: Math.sin, fn2: () => null, fn3: new Function('x', 'y', 'return x * y'), }; /* tslint:enable */ expect(truncate(obj)).toStrictEqual({ foo: 'bar' }); }); it('sets object type', () => { let e = new Event('load'); let got = truncate(e); expect(got.__type).toBe('Event'); }); describe('when called with object with circular references', () => { let obj = { foo: 'bar' }; obj.circularRef = obj; obj.circularList = [obj, obj]; let truncated; beforeEach(() => { truncated = truncate(obj); }); it('produces object with resolved circular references', () => { expect(truncated).toStrictEqual({ foo: 'bar', circularRef: '[Circular ~]', circularList: ['[Circular ~]', '[Circular ~]'], }); }); }); describe('when called with object with complex circular references', () => { let a = { x: 1 }; a.a = a; let b = { x: 2 }; b.a = a; let c = { a, b }; let obj = { list: [a, b, c] }; obj.obj = obj; let truncated; beforeEach(() => { truncated = truncate(obj); }); it('produces object with resolved circular references', () => { expect(truncated).toStrictEqual({ list: [ { x: 1, a: '[Circular ~.list.0]', }, { x: 2, a: '[Circular ~.list.0]', }, { a: '[Circular ~.list.0]', b: '[Circular ~.list.1]', }, ], obj: '[Circular ~]', }); }); }); describe('when called with deeply nested objects', () => { let obj = {}; let tmp = obj; for (let i = 0; i < 100; i++) { tmp.value = i; tmp.obj = {}; tmp = tmp.obj; } let truncated; beforeEach(() => { truncated = truncate(obj, { level: 1 }); }); it('produces truncated object', () => { expect(truncated).toStrictEqual({ value: 0, obj: { value: 1, obj: { value: 2, obj: { value: 3, obj: '[Truncated Object]', }, }, }, }); }); }); describe('when called with object created with Object.create(null)', () => { it('works', () => { let obj = Object.create(null); obj.foo = 'bar'; expect(truncate(obj)).toStrictEqual({ foo: 'bar' }); }); }); describe('keysBlocklist', () => { it('filters blocklisted keys', () => { let obj = { params: { password: '123', sub: { secret: '123', }, }, }; let keysBlocklist = [/password/, /secret/]; let truncated = truncate(obj, { keysBlocklist }); expect(truncated).toStrictEqual({ params: { password: '[Filtered]', sub: { secret: '[Filtered]' }, }, }); }); }); }); ================================================ FILE: packages/browser/tsconfig.cjs.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist" }, "include": ["src"] } ================================================ FILE: packages/browser/tsconfig.esm.json ================================================ { "extends": "../../tsconfig.esm.json", "compilerOptions": { "outDir": "esm" }, "include": ["src"] } ================================================ FILE: packages/browser/tsconfig.json ================================================ { "extends": "./tsconfig.cjs.json", "compilerOptions": { "rootDir": ".", }, "include": ["src", "test"] } ================================================ FILE: packages/browser/tsconfig.umd.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "declaration": false, "declarationMap": false, "module": "ES6", } } ================================================ FILE: packages/browser/tslint.json ================================================ { "extends": ["../../tslint.json"] } ================================================ FILE: packages/node/LICENSE ================================================ MIT License Copyright (c) 2020 Airbrake Technologies, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/node/README.md ================================================

# Official Airbrake Notifier for Node.js [![Build Status](https://github.com/airbrake/airbrake-js/workflows/CI/badge.svg?branch=master)](https://github.com/airbrake/airbrake-js/actions?query=branch%3Amaster) [![npm version](https://img.shields.io/npm/v/@airbrake/node.svg)](https://www.npmjs.com/package/@airbrake/node) [![npm dm](https://img.shields.io/npm/dm/@airbrake/node.svg)](https://www.npmjs.com/package/@airbrake/node) [![npm dt](https://img.shields.io/npm/dt/@airbrake/node.svg)](https://www.npmjs.com/package/@airbrake/node) The official Airbrake notifier for capturing JavaScript errors in Node.js and reporting them to [Airbrake](http://airbrake.io). If you're looking for browser support, there is a [separate package](https://github.com/airbrake/airbrake-js/tree/master/packages/browser). ## Installation Using yarn: ```sh yarn add @airbrake/node ``` Using npm: ```sh npm install @airbrake/node ``` ## Basic Usage First, initialize the notifier with the project ID and project key taken from [Airbrake](https://airbrake.io). To find your `project_id` and `project_key` navigate to your project's _Settings_ and copy the values from the right sidebar: ![][project-idkey] ```js const { Notifier } = require('@airbrake/node'); const airbrake = new Notifier({ projectId: 1, projectKey: 'REPLACE_ME', environment: 'production', }); ``` Then, you can send a textual message to Airbrake: ```js let promise = airbrake.notify(`user id=${user_id} not found`); promise.then((notice) => { if (notice.id) { console.log('notice id', notice.id); } else { console.log('notify failed', notice.error); } }); ``` or report errors directly: ```js try { throw new Error('Hello from Airbrake!'); } catch (err) { airbrake.notify(err); } ``` Alternatively, you can wrap any code which may throw errors using the `wrap` method: ```js let startApp = () => { throw new Error('Hello from Airbrake!'); }; startApp = airbrake.wrap(startApp); // Any exceptions thrown in startApp will be reported to Airbrake. startApp(); ``` or use the `call` shortcut: ```js let startApp = () => { throw new Error('Hello from Airbrake!'); }; airbrake.call(startApp); ``` ## Example configurations - [Express](examples/express) - [Node.js](examples/nodejs) ## Advanced Usage ### Notice Annotations It's possible to annotate error notices with all sorts of useful information at the time they're captured by supplying it in the object being reported. ```js try { startApp(); } catch (err) { airbrake.notify({ error: err, context: { component: 'bootstrap' }, environment: { env1: 'value' }, params: { param1: 'value' }, session: { session1: 'value' }, }); } ``` ### Severity [Severity](https://airbrake.io/docs/airbrake-faq/what-is-severity/) allows categorizing how severe an error is. By default, it's set to `error`. To redefine severity, simply overwrite `context/severity` of a notice object: ```js airbrake.notify({ error: err, context: { severity: 'warning' }, }); ``` ### Filtering errors There may be some errors thrown in your application that you're not interested in sending to Airbrake, such as errors thrown by 3rd-party libraries. The Airbrake notifier makes it simple to ignore this chaff while still processing legitimate errors. Add filters to the notifier by providing filter functions to `addFilter`. `addFilter` accepts the entire [error notice](https://airbrake.io/docs/api/#create-notice-v3) to be sent to Airbrake and provides access to the `context`, `environment`, `params`, and `session` properties. It also includes the single-element `errors` array with its `backtrace` property and associated backtrace lines. The return value of the filter function determines whether or not the error notice will be submitted. - If `null` is returned, the notice is ignored. - Otherwise, the returned notice will be submitted. An error notice must pass all provided filters to be submitted. In the following example all errors triggered by admins will be ignored: ```js airbrake.addFilter((notice) => { if (notice.params.admin) { // Ignore errors from admin sessions. return null; } return notice; }); ``` Filters can be also used to modify notice payload, e.g. to set the environment and application version: ```js airbrake.addFilter((notice) => { notice.context.environment = 'production'; notice.context.version = '1.2.3'; return notice; }); ``` ### Filtering keys With the `keysBlocklist` option, you can specify a list of keys containing sensitive information that must be filtered out: ```js const airbrake = new Notifier({ // ... keysBlocklist: [ 'password', // exact match /secret/, // regexp match ], }); ``` ### Node.js request and proxy To use the [request](https://github.com/request/request) HTTP client, pass the `request` option which accepts a request wrapper: ```js const airbrake = new Notifier({ // ... request: request.defaults({ proxy: 'http://localproxy.com' }), }); ``` ### Instrumentation `@airbrake/node` attempts to automatically instrument various performance metrics. You can disable that behavior using the `performanceStats` option: ```js const airbrake = new Notifier({ // ... performanceStats: false, }); ``` ### Filtering performance data `addPerformanceFilter` allows for filtering performance data. Return `null` in the filter to prevent that metric from being reported to Airbrake. ```js airbrake.addPerformanceFilter((metric) => { if (metric.route === '/foo') { // Requests to '/foo' will not be reported return null; } return metric; }); ``` [project-idkey]: https://s3.amazonaws.com/airbrake-github-assets/airbrake-js/project-id-key.png ================================================ FILE: packages/node/babel.config.js ================================================ module.exports = { presets: [['@babel/preset-env', { targets: { node: 'current' } }]], }; ================================================ FILE: packages/node/examples/express/README.md ================================================ # Using Airbrake with Express.js This example Node.js application uses Express.js and sets up Airbrake to report errors and performance data. To adapt this example to your app, follow these steps: #### 1. Install the package ```shell npm install @airbrake/node ``` #### 2. Include @airbrake/node and the Express.js instrumentation in your app Include the required Airbrake libraries in your `app.js` ```js const Airbrake = require('@airbrake/node'); const airbrakeExpress = require('@airbrake/node/dist/instrumentation/express'); ``` #### 3. Configure Airbrake with your project's credentials ```js const airbrake = new Airbrake.Notifier({ projectId: process.env.AIRBRAKE_PROJECT_ID, projectKey: process.env.AIRBRAKE_PROJECT_KEY, }); ``` #### 4. Add the Airbrake Express middleware This middleware should be added before any routes are defined. ```js app.use(airbrakeExpress.makeMiddleware(airbrake)); ``` #### 5. Add the Airbrake Express error handler The error handler middleware should be defined last. For more info on how this works, see the official [Express error handling doc](http://expressjs.com/en/guide/error-handling.html). ```js app.use(airbrakeExpress.makeErrorHandler(airbrake)); ``` #### 6. Run your app The last step is to run your app. To test that you've configured Airbrake correctly, you can throw an error inside any of your routes: ```js app.get('/hello/:name', function hello(_req, _res) { throw new Error('Hello from Airbrake!'); }); ``` Any unhandled errors that are thrown will now be reported to Airbrake. See the [basic usage](https://github.com/airbrake/airbrake-js/tree/master/packages/node#basic-usage) to learn how to manually send errors to Airbrake. **Note:** to see this all in action, take a look at our [example `app.js` file](https://github.com/airbrake/airbrake-js/blob/master/packages/node/examples/express/app.js) and to run the example, follow the next steps. # Running the example app If you want to run this example application locally, follow these steps: #### 1. Clone the airbrake-js repo: ```shell git clone git@github.com:airbrake/airbrake-js.git ``` #### 2. Navigate to this directory: ``` cd airbrake-js/packages/node/examples/express ``` #### 3. Run the following commands while providing your `project ID` and `project API key` ```shell npm install AIRBRAKE_PROJECT_ID=your-id AIRBRAKE_PROJECT_KEY=your-key node app.js firefox localhost:3000 ``` ================================================ FILE: packages/node/examples/express/app.js ================================================ const express = require('express'); const pg = require('pg'); const Airbrake = require('@airbrake/node'); const airbrakeExpress = require('@airbrake/node/dist/instrumentation/express'); async function main() { const airbrake = new Airbrake.Notifier({ projectId: process.env.AIRBRAKE_PROJECT_ID, projectKey: process.env.AIRBRAKE_PROJECT_KEY, }); const client = new pg.Client(); await client.connect(); const app = express(); // This middleware should be added before any routes are defined. app.use(airbrakeExpress.makeMiddleware(airbrake)); app.get('/', async function home(req, res) { const result = await client.query('SELECT $1::text as message', [ 'Hello world!', ]); console.log(result.rows[0].message); res.send('Hello World!'); }); app.get('/hello/:name', function hello(_req, _res) { throw new Error('Hello from Airbrake!'); }); // Error handler middleware should be the last one. // See http://expressjs.com/en/guide/error-handling.html app.use(airbrakeExpress.makeErrorHandler(airbrake)); app.listen(3000, function() { console.log('Example app listening on port 3000!'); }); } main(); ================================================ FILE: packages/node/examples/express/package.json ================================================ { "name": "airbrake-example", "dependencies": { "@airbrake/node": "^2.1.9", "express": "^4.17.1", "pg": "^8.0.0" } } ================================================ FILE: packages/node/examples/nodejs/README.md ================================================ # Using Airbrake with Node.js #### 1. Install the package ```shell npm install @airbrake/node ``` #### 2. Include @airbrake/node in your app Include the required Airbrake libraries in your `app.js` ```js const Airbrake = require('@airbrake/node'); ``` #### 3. Configure Airbrake with your project's credentials ```js const airbrake = new Airbrake.Notifier({ projectId: process.env.AIRBRAKE_PROJECT_ID, projectKey: process.env.AIRBRAKE_PROJECT_KEY, }); ``` #### 4. Run your app The last step is to run your app. To test that you've configured Airbrake correctly, you can throw an error inside any of your routes: ```js const hostname = '127.0.0.1'; const port = 3000; const server = http.createServer((_req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello World'); throw new Error('I am an uncaught exception'); }); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); }); ``` Any unhandled errors that are thrown will now be reported to Airbrake. See the [basic usage](https://github.com/airbrake/airbrake-js/tree/master/packages/node#basic-usage) to learn how to manually send errors to Airbrake. **Note:** to see this all in action, take a look at our [example `app.js` file](https://github.com/airbrake/airbrake-js/blob/master/packages/node/examples/nodejs/app.js) and to run the example, follow the next steps. # Running the example app If you want to run this example application locally, follow these steps: #### 1. Clone the airbrake-js repo: ```shell git clone git@github.com:airbrake/airbrake-js.git ``` #### 2. Navigate to this directory: ``` cd airbrake-js/packages/node/examples/nodejs ``` #### 3. Run the following commands while providing your `project ID` and `project API key` ```shell npm install AIRBRAKE_PROJECT_ID=your-id AIRBRAKE_PROJECT_KEY=your-key node app.js firefox localhost:3000 ``` ================================================ FILE: packages/node/examples/nodejs/app.js ================================================ const http = require('http'); const Airbrake = require('@airbrake/node'); new Airbrake.Notifier({ projectId: process.env.AIRBRAKE_PROJECT_ID, projectKey: process.env.AIRBRAKE_PROJECT_KEY, }); const hostname = '127.0.0.1'; const port = 3000; const server = http.createServer((_req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello World'); throw new Error('I am an uncaught exception'); }); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); }); ================================================ FILE: packages/node/examples/nodejs/package.json ================================================ { "name": "airbrake-example", "dependencies": { "@airbrake/node": "^2.1.9" } } ================================================ FILE: packages/node/jest.config.js ================================================ module.exports = { transform: { '^.+\\.jsx?$': 'babel-jest', '^.+\\.tsx?$': 'ts-jest', }, testEnvironment: 'node', moduleNameMapper: { '^@airbrake/(.*)$': '/../$1/src', }, roots: ['tests'], clearMocks: true, }; ================================================ FILE: packages/node/package.json ================================================ { "name": "@airbrake/node", "version": "2.1.9", "description": "Official Airbrake notifier for Node.js", "author": "Airbrake", "license": "MIT", "repository": { "type": "git", "url": "git://github.com/airbrake/airbrake-js.git", "directory": "packages/node" }, "homepage": "https://github.com/airbrake/airbrake-js/tree/master/packages/node", "keywords": [ "exception", "error", "airbrake", "notifier" ], "engines": { "node": ">=10" }, "dependencies": { "@airbrake/browser": "^2.1.9", "cross-fetch": "^3.1.5", "error-stack-parser": "^2.0.4", "tdigest": "^0.1.1" }, "devDependencies": { "@babel/core": "^7.9.0", "@babel/preset-env": "^7.9.0", "babel-jest": "^29.3.1", "jest": "^27.3.1", "prettier": "^2.0.2", "ts-jest": "^27.1.0", "tslint": "^6.1.0", "tslint-config-prettier": "^1.18.0", "tslint-plugin-prettier": "^2.3.0", "typescript": "^4.0.2" }, "main": "dist/index.js", "module": "esm/index.js", "files": [ "dist/", "esm/", "README.md", "LICENSE" ], "scripts": { "build": "yarn build:cjs && yarn build:esm", "build:watch": "concurrently 'yarn build:cjs:watch' 'yarn build:esm:watch'", "build:cjs": "tsc -p tsconfig.cjs.json", "build:cjs:watch": "tsc -p tsconfig.cjs.json -w --preserveWatchOutput", "build:esm": "tsc -p tsconfig.esm.json", "build:esm:watch": "tsc -p tsconfig.esm.json -w --preserveWatchOutput", "clean": "rm -rf dist esm", "lint": "tslint -p .", "test": "jest" } } ================================================ FILE: packages/node/src/filter/node.ts ================================================ import { INotice } from '@airbrake/browser'; import { NOTIFIER_NAME, NOTIFIER_VERSION, NOTIFIER_URL } from '../version'; const os = require('os'); export function nodeFilter(notice: INotice): INotice { if (notice.context.notifier) { notice.context.notifier.name = NOTIFIER_NAME; notice.context.notifier.version = NOTIFIER_VERSION; notice.context.notifier.url = NOTIFIER_URL; } notice.context.os = `${os.type()}/${os.release()}`; notice.context.architecture = os.arch(); notice.context.hostname = os.hostname(); notice.params.os = { homedir: os.homedir(), uptime: os.uptime(), freemem: os.freemem(), totalmem: os.totalmem(), loadavg: os.loadavg(), }; notice.context.platform = process.platform; if (!notice.context.rootDirectory) { notice.context.rootDirectory = process.cwd(); } notice.params.process = { pid: process.pid, cwd: process.cwd(), execPath: process.execPath, argv: process.argv, }; ['uptime', 'cpuUsage', 'memoryUsage'].map((name) => { if (process[name]) { notice.params.process[name] = process[name](); } }); return notice; } ================================================ FILE: packages/node/src/index.ts ================================================ export { Notifier } from './notifier'; ================================================ FILE: packages/node/src/instrumentation/debug.ts ================================================ import { Notifier } from '../notifier'; export function patch(createDebug, airbrake: Notifier): void { const oldInit = createDebug.init; createDebug.init = function (debug) { oldInit.apply(this, arguments); const oldLog = debug.log || createDebug.log; debug.log = function abCreateDebug() { airbrake.scope().pushHistory({ type: 'log', arguments, }); return oldLog.apply(this, arguments); }; }; } ================================================ FILE: packages/node/src/instrumentation/express.ts ================================================ import { Notifier } from '../notifier'; export function makeMiddleware(airbrake: Notifier) { return function airbrakeMiddleware(req, res, next): void { const route = req.route?.path?.toString() ?? 'UNKNOWN'; const metric = airbrake.routes.start(req.method, route); if (!metric.isRecording()) { next(); return; } const origEnd = res.end; res.end = function abEnd() { metric.route = req.route?.path?.toString() ?? 'UNKNOWN'; metric.statusCode = res.statusCode; metric.contentType = res.get('Content-Type'); airbrake.routes.notify(metric); return origEnd.apply(this, arguments); }; next(); }; } export function makeErrorHandler(airbrake: Notifier) { return function airbrakeErrorHandler(err: Error, req, _res, next): void { const url = req.protocol + '://' + req.headers.host + req.originalUrl; const notice: any = { error: err, context: { userAddr: req.ip, userAgent: req.headers['user-agent'], url, httpMethod: req.method, component: 'express', }, }; if (req.route) { if (req.route.path) { notice.context.route = req.route.path.toString(); } if (req.route.stack && req.route.stack.length) { notice.context.action = req.route.stack[0].name; } } const referer = req.headers.referer; if (referer) { notice.context.referer = referer; } airbrake.notify(notice); next(err); }; } ================================================ FILE: packages/node/src/instrumentation/http.ts ================================================ import { Notifier } from '../notifier'; const SPAN_NAME = 'http'; export function patch(http, airbrake: Notifier): void { if (http.request) { http.request = wrapRequest(http.request, airbrake); } if (http.get) { http.get = wrapRequest(http.get, airbrake); } } export function wrapRequest(origFn, airbrake: Notifier) { return function abRequest() { const metric = airbrake.scope().routeMetric(); metric.startSpan(SPAN_NAME); const req = origFn.apply(this, arguments); if (!metric.isRecording()) { return req; } const origEmit = req.emit; req.emit = function (type, _res) { if (type === 'response') { metric.endSpan(SPAN_NAME); } return origEmit.apply(this, arguments); }; return req; }; } ================================================ FILE: packages/node/src/instrumentation/https.ts ================================================ import { Notifier } from '../notifier'; import { wrapRequest } from './http'; export function patch(https, airbrake: Notifier): void { if (https.request) { https.request = wrapRequest(https.request, airbrake); } if (https.get) { https.get = wrapRequest(https.get, airbrake); } } ================================================ FILE: packages/node/src/instrumentation/mysql.ts ================================================ import { QueryInfo } from '@airbrake/browser'; import { Notifier } from '../notifier'; const SPAN_NAME = 'sql'; export function patch(mysql, airbrake: Notifier): void { mysql.createPool = wrapCreatePool(mysql.createPool, airbrake); const origCreatePoolCluster = mysql.createPoolCluster; mysql.createPoolCluster = function abCreatePoolCluster() { const cluster = origCreatePoolCluster.apply(this, arguments); cluster.of = wrapCreatePool(cluster.of, airbrake); return cluster; }; const origCreateConnection = mysql.createConnection; mysql.createConnection = function abCreateConnection() { const conn = origCreateConnection.apply(this, arguments); wrapConnection(conn, airbrake); return conn; }; } function wrapCreatePool(origFn, airbrake: Notifier) { return function abCreatePool() { const pool = origFn.apply(this, arguments); pool.getConnection = wrapGetConnection(pool.getConnection, airbrake); return pool; }; } function wrapGetConnection(origFn, airbrake: Notifier) { return function abGetConnection() { const cb = arguments[0]; if (typeof cb === 'function') { arguments[0] = function abCallback(_err, conn) { if (conn) { wrapConnection(conn, airbrake); } return cb.apply(this, arguments); }; } return origFn.apply(this, arguments); }; } function wrapConnection(conn, airbrake: Notifier): void { const origQuery = conn.query; conn.query = function abQuery(sql, values, cb) { let foundCallback = false; function wrapCallback(callback) { foundCallback = true; return function abCallback() { endSpan(); return callback.apply(this, arguments); }; } const metric = airbrake.scope().routeMetric(); if (!metric.isRecording()) { return origQuery.apply(this, arguments); } metric.startSpan(SPAN_NAME); let qinfo: QueryInfo; const endSpan = () => { metric.endSpan(SPAN_NAME); if (qinfo) { airbrake.queries.notify(qinfo); } }; let query: string; switch (typeof sql) { case 'string': query = sql; break; case 'function': arguments[0] = wrapCallback(sql); break; case 'object': if (typeof sql._callback === 'function') { sql._callback = wrapCallback(sql._callback); } query = sql.sql; break; } if (query) { qinfo = airbrake.queries.start(query); } if (typeof values === 'function') { arguments[1] = wrapCallback(values); } else if (typeof cb === 'function') { arguments[2] = wrapCallback(cb); } const res = origQuery.apply(this, arguments); if (!foundCallback && res && res.emit) { const origEmit = res.emit; res.emit = function abEmit(evt) { switch (evt) { case 'end': case 'error': endSpan(); break; } return origEmit.apply(this, arguments); }; } return res; }; } ================================================ FILE: packages/node/src/instrumentation/mysql2.ts ================================================ import { QueryInfo } from '@airbrake/browser'; import { Notifier } from '../notifier'; const SPAN_NAME = 'sql'; export function patch(mysql2, airbrake: Notifier): void { const proto = mysql2.Connection.prototype; proto.query = wrapQuery(proto.query, airbrake); proto.execute = wrapQuery(proto.execute, airbrake); } function wrapQuery(origQuery, airbrake: Notifier) { return function abQuery(sql, values, cb) { const metric = airbrake.scope().routeMetric(); if (!metric.isRecording()) { return origQuery.apply(this, arguments); } metric.startSpan(SPAN_NAME); let qinfo: QueryInfo; const endSpan = () => { metric.endSpan(SPAN_NAME); if (qinfo) { airbrake.queries.notify(qinfo); } }; let foundCallback = false; function wrapCallback(callback) { foundCallback = true; return function abCallback() { endSpan(); return callback.apply(this, arguments); }; } let query: string; switch (typeof sql) { case 'string': query = sql; break; case 'function': arguments[0] = wrapCallback(sql); break; case 'object': if (typeof sql.onResult === 'function') { sql.onResult = wrapCallback(sql.onResult); } query = sql.sql; break; } if (query) { qinfo = airbrake.queries.start(query); } if (typeof values === 'function') { arguments[1] = wrapCallback(values); } else if (typeof cb === 'function') { arguments[2] = wrapCallback(cb); } const res = origQuery.apply(this, arguments); if (!foundCallback && res && res.emit) { const origEmit = res.emit; res.emit = function abEmit(evt) { switch (evt) { case 'end': case 'error': case 'close': endSpan(); break; } return origEmit.apply(this, arguments); }; } return res; }; } ================================================ FILE: packages/node/src/instrumentation/pg.ts ================================================ import { QueryInfo } from '@airbrake/browser'; import { Notifier } from '../notifier'; const SPAN_NAME = 'sql'; export function patch(pg, airbrake: Notifier): void { patchClient(pg.Client, airbrake); const origGetter = pg.__lookupGetter__('native'); if (origGetter) { delete pg.native; pg.__defineGetter__('native', () => { const native = origGetter(); if (native && native.Client) { patchClient(native.Client, airbrake); } return native; }); } } // tslint:disable-next-line: variable-name function patchClient(Client, airbrake: Notifier): void { const origQuery = Client.prototype.query; Client.prototype.query = function abQuery(sql) { const metric = airbrake.scope().routeMetric(); if (!metric.isRecording()) { return origQuery.apply(this, arguments); } metric.startSpan(SPAN_NAME); if (sql && typeof sql.text === 'string') { sql = sql.text; } let qinfo: QueryInfo; if (typeof sql === 'string') { qinfo = airbrake.queries.start(sql); } let cbIdx = arguments.length - 1; let cb = arguments[cbIdx]; if (Array.isArray(cb)) { cbIdx = cb.length - 1; cb = cb[cbIdx]; } const endSpan = () => { metric.endSpan(SPAN_NAME); if (qinfo) { airbrake.queries.notify(qinfo); } }; if (typeof cb === 'function') { arguments[cbIdx] = function abCallback() { endSpan(); return cb.apply(this, arguments); }; return origQuery.apply(this, arguments); } const query = origQuery.apply(this, arguments); if (typeof query.on === 'function') { query.on('end', endSpan); query.on('error', endSpan); } else if ( typeof query.then === 'function' && typeof query.catch === 'function' ) { query.then(endSpan).catch(endSpan); } return query; }; } ================================================ FILE: packages/node/src/instrumentation/redis.ts ================================================ import { Notifier } from '../notifier'; const SPAN_NAME = 'redis'; export function patch(redis, airbrake: Notifier): void { const proto = redis.RedisClient.prototype; const origSendCommand = proto.internal_send_command; proto.internal_send_command = function ab_internal_send_command(cmd) { const metric = airbrake.scope().routeMetric(); metric.startSpan(SPAN_NAME); if (!metric.isRecording()) { return origSendCommand.apply(this, arguments); } if (cmd && cmd.callback) { const origCb = cmd.callback; cmd.callback = function abCallback() { metric.endSpan(SPAN_NAME); return origCb.apply(this, arguments); }; } return origSendCommand.apply(this, arguments); }; } ================================================ FILE: packages/node/src/notifier.ts ================================================ import { BaseNotifier, INotice, IOptions } from '@airbrake/browser'; import { nodeFilter } from './filter/node'; import { Scope, ScopeManager } from './scope'; export class Notifier extends BaseNotifier { _inFlight: number; _scopeManager?: ScopeManager; _mainScope?: Scope; constructor(opt: IOptions) { if (!opt.environment && process.env.NODE_ENV) { opt.environment = process.env.NODE_ENV; } super(opt); this.addFilter(nodeFilter); this._inFlight = 0; process.on('beforeExit', async () => { await this.flush(); }); process.on('uncaughtException', (err) => { this.notify(err).then(() => { if (process.listeners('uncaughtException').length !== 1) { return; } if (console.error) { console.error('uncaught exception', err); } process.exit(1); }); }); process.on('unhandledRejection', (reason: Error, _p) => { let msg = reason.message || String(reason); if (msg.indexOf && msg.indexOf('airbrake: ') === 0) { return; } this.notify(reason).then(() => { if (process.listeners('unhandledRejection').length !== 1) { return; } if (console.error) { console.error('unhandled rejection', reason); } process.exit(1); }); }); if (opt.performanceStats) { this._instrument(); this._scopeManager = new ScopeManager(); } this._mainScope = new Scope(); } scope(): Scope { if (this._scopeManager) { const scope = this._scopeManager.active(); if (scope) { return scope; } } return this._mainScope; } setActiveScope(scope: Scope) { this._scopeManager.setActive(scope); } notify(err: any): Promise { this._inFlight++; return super.notify(err).finally(() => { this._inFlight--; }); } async flush(timeout = 3000): Promise { if (this._inFlight === 0 || timeout <= 0) { return Promise.resolve(true); } return new Promise((resolve, _reject) => { let interval = timeout / 100; if (interval <= 0) { interval = 10; } const timerID = setInterval(() => { if (this._inFlight === 0) { resolve(true); clearInterval(timerID); return; } if (timeout <= 0) { resolve(false); clearInterval(timerID); return; } timeout -= interval; }, interval); }); } _instrument() { const mods = ['pg', 'mysql', 'mysql2', 'redis', 'http', 'https']; for (let modName of mods) { try { const mod = require(`${modName}.js`); const airbrakeMod = require(`@airbrake/node/dist/instrumentation/${modName}.js`); airbrakeMod.patch(mod, this); } catch (_) {} } } } ================================================ FILE: packages/node/src/scope.ts ================================================ import { Scope } from '@airbrake/browser'; import * as asyncHooks from 'async_hooks'; export { Scope }; export class ScopeManager { _asyncHook: asyncHooks.AsyncHook; _scopes: { [id: number]: Scope } = {}; constructor() { this._asyncHook = asyncHooks .createHook({ init: this._init.bind(this), destroy: this._destroy.bind(this), promiseResolve: this._destroy.bind(this), }) .enable(); } setActive(scope: Scope) { const eid = asyncHooks.executionAsyncId(); this._scopes[eid] = scope; } active(): Scope { const eid = asyncHooks.executionAsyncId(); return this._scopes[eid]; } _init(aid: number) { this._scopes[aid] = this._scopes[asyncHooks.executionAsyncId()]; } _destroy(aid: number) { delete this._scopes[aid]; } } ================================================ FILE: packages/node/src/version.ts ================================================ export const NOTIFIER_NAME = 'airbrake-js/node'; export const NOTIFIER_VERSION = '2.1.9'; export const NOTIFIER_URL = 'https://github.com/airbrake/airbrake-js/tree/master/packages/node'; ================================================ FILE: packages/node/tests/notifier.test.js ================================================ import { Notifier } from '../src/notifier'; describe('Notifier', () => { describe('configuration', () => { describe('performanceStats', () => { test('is enabled by default', () => { const notifier = new Notifier({ projectId: 1, projectKey: 'key', remoteConfig: false, }); expect(notifier._opt.performanceStats).toEqual(true); }); test('sets up instrumentation when enabled', () => { Notifier.prototype._instrument = jest.fn(); const notifier = new Notifier({ projectId: 1, projectKey: 'key', remoteConfig: false, }); expect(notifier._instrument.mock.calls.length).toEqual(1); }); test('can be disabled', () => { const notifier = new Notifier({ projectId: 1, projectKey: 'key', performanceStats: false, remoteConfig: false, }); expect(notifier._opt.performanceStats).toEqual(false); }); test('does not set up instrumentation when disabled', () => { Notifier.prototype._instrument = jest.fn(); const notifier = new Notifier({ projectId: 1, projectKey: 'key', performanceStats: false, remoteConfig: false, }); expect(notifier._instrument.mock.calls.length).toEqual(0); }); }); }); }); ================================================ FILE: packages/node/tests/routes.test.js ================================================ import { Notifier } from '../src/notifier'; describe('Routes', () => { const opt = { projectId: 1, projectKey: 'test', remoteConfig: false, }; let notifier; let routes; let req; beforeEach(() => { notifier = new Notifier(opt); routes = notifier.routes; req = routes.start('GET', '/projects/:id'); req.statusCode = 200; req.contentType = 'application/json'; req.startTime = new Date(1); req.endTime = new Date(1000); }); it('collects metrics to report to Airbrake', () => { routes.notify(req); clearTimeout(routes._routes._timer); clearTimeout(routes._breakdowns._timer); let m = JSON.parse(JSON.stringify(routes._routes._m)); expect(m).toStrictEqual({ '{"method":"GET","route":"/projects/:id","statusCode":200,"time":"1970-01-01T00:00:00.000Z"}': { count: 1, sum: 999, sumsq: 998001, tdigestCentroids: { count: [1], mean: [999] }, }, }); }); it('does not collect metrics that are filtered', () => { notifier.addPerformanceFilter(() => null); routes.notify(req); clearTimeout(routes._routes._timer); clearTimeout(routes._breakdowns._timer); expect(routes._routes._m).toStrictEqual({}); }); }); ================================================ FILE: packages/node/tsconfig.cjs.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist" }, "include": ["src"] } ================================================ FILE: packages/node/tsconfig.esm.json ================================================ { "extends": "../../tsconfig.esm.json", "compilerOptions": { "outDir": "esm" }, "include": ["src"] } ================================================ FILE: packages/node/tsconfig.json ================================================ { "extends": "./tsconfig.cjs.json", "compilerOptions": { "rootDir": ".", }, "include": ["src", "test"] } ================================================ FILE: packages/node/tslint.json ================================================ { "extends": ["../../tslint.json"] } ================================================ FILE: tsconfig.esm.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "module": "ES6" } } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "declaration": true, "declarationMap": true, "esModuleInterop": true, "lib": ["ES2015", "DOM"], "moduleResolution": "node", "noImplicitReturns": true, "noUnusedLocals": true, "noUnusedParameters": true, "preserveConstEnums": true, "skipLibCheck": true, "sourceMap": true, "target": "ES5" } } ================================================ FILE: tslint.json ================================================ { "extends": [ "tslint:latest", "tslint-config-prettier", "tslint-plugin-prettier" ], "rules": { "max-classes-per-file": false, "member-access": false, "member-ordering": false, "no-bitwise": false, "no-console": [true, "log"], "no-empty": false, "no-implicit-dependencies": false, "no-shadowed-variable": [true, { "temporalDeadZone": false }], "no-submodule-imports": [true, "promise-polyfill/src/polyfill"], "no-var-requires": false, "object-literal-sort-keys": false, "prefer-const": false, "prettier": true, "variable-name": [true, "allow-leading-underscore"] } }