Showing preview only (217K chars total). Download the full file or copy to clipboard to get everything.
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
<!-- Can you identify one or more @airbrake/* packages as the source of the bug? -->
<!-- ✍️edit: --> The issue is caused by package @airbrake/....
### Is this a regression?
<!-- Did this behavior use to work in the previous version? -->
<!-- ✍️--> Yes, the previous version in which this bug was not present was: ....
### Description
<!-- ✍️--> A clear and concise description of the problem...
## 🔬 Minimal Reproduction
<!-- Please create and share minimal reproduction of the issue. -->
<!-- A gist or sample app repo are both great. -->
<!-- ✍️--> https://gist.github.com/...
<!--
Share the link to the repo along with step-by-step instructions to reproduce the problem, as well as expected and actual behavior.
Issues that don't have enough info and can't be reproduced will be closed.
-->
## 🔥 Exception or Error
<pre><code>
<!-- If the issue is accompanied by an exception or an error, please share it below: -->
<!-- ✍️-->
</code></pre>
## 🌍 Your Environment
**@airbrake/\* version:**
<pre><code>
<!-- Copy and paste your @airbrake/* package version below -->
<!-- ✍️-->
</code></pre>
**Anything else relevant?**
<!-- ✍️Is this a browser specific issue? If so, please specify the browser and version. -->
<!-- ✍️Do any of these matter: build configuration (Webpack, Angular CLI, Parcel, Rollup, etc.), operating system, package manager, ...? If so, please mention it below. -->
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest a feature for Airbrake
title: ''
labels: enhancement
assignees: ''
---
# 🚀 feature request
### Relevant Package
<!-- Can you identify one or more @airbrake/* packages that are relevant for this feature request? -->
<!-- ✍️edit: --> 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
================================================
<p align="center">
<img src="https://airbrake-github-assets.s3.amazonaws.com/brand/airbrake-full-logo.png" width="200">
</p>
# Official Airbrake Notifiers for JavaScript
[](https://github.com/airbrake/airbrake-js/actions?query=branch%3Amaster)
[](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
================================================
<p align="center">
<img src="https://airbrake-github-assets.s3.amazonaws.com/brand/airbrake-full-logo.png" width="200">
</p>
# Official Airbrake Notifier for Browsers
[](https://github.com/airbrake/airbrake-js/actions?query=branch%3Amaster)
[](https://www.npmjs.com/package/@airbrake/browser)
[](https://www.npmjs.com/package/@airbrake/browser)
[](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 `<script>` tag via jsdelivr:
```html
<script src="https://cdn.jsdelivr.net/npm/@airbrake/browser"></script>
```
Using a `<script>` tag via unpkg:
```html
<script src="https://unpkg.com/@airbrake/browser"></script>
```
## 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
<!-- index.html -->
<!DOCTYPE html>
<html ng-app="app">
<head>
<meta charset="utf 8" />
<title>Hello World</title>
</head>
<body>
<h1 ng-controller="HelloWorldCtrl">{{message}}</h1>
<script src="https://code.angularjs.org/1.8.2/angular.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@airbrake/browser"></script>
<script src="app.js"></script>
</body>
</html>
```
[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
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Airbrake legacy example</title>
<script src="https://cdn.jsdelivr.net/npm/@airbrake/browser"></script>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="app.js"></script>
</head>
<body>
<input id="error_text" />
<button id="send_error">Send error to Airbrake</button>
</body>
</html>
================================================
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 <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;
```
Then, you can use it as a regular component:
```html
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
```
## 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 (
<p>
{statusCode
? `An error ${statusCode} occurred on server`
: 'An error occurred on client'}
</p>
)
}
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 <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>
}
================================================
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 <h1>Something went wrong.</h1>;
}
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 (
<div className={styles.container}>
<Head>
<link rel="icon" href="/favicon.ico" />
<meta
name="description"
content="Learn how to build a personal website using Next.js"
/>
<meta
property="og:image"
content={`https://og-image.vercel.app/${encodeURI(
siteTitle
)}.png?theme=light&md=0&fontSize=75px&images=https%3A%2F%2Fassets.zeit.co%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg`}
/>
<meta name="og:title" content={siteTitle} />
<meta name="twitter:card" content="summary_large_image" />
</Head>
<header className={styles.header}>
{home ? (
<>
<Image
priority
src="/images/profile.jpg"
className={utilStyles.borderCircle}
height={144}
width={144}
alt={name}
/>
<h1 className={utilStyles.heading2Xl}>{name}</h1>
</>
) : (
<>
<Link href="/">
<a>
<Image
priority
src="/images/profile.jpg"
className={utilStyles.borderCircle}
height={108}
width={108}
alt={name}
/>
</a>
</Link>
<h2 className={utilStyles.headingLg}>
<Link href="/">
<a className={utilStyles.colorInherit}>{name}</a>
</Link>
</h2>
</>
)}
</header>
<main>{children}</main>
{!home && (
<div className={styles.backToHome}>
<Link href="/">
<a>← Back to home</a>
</Link>
</div>
)}
</div>
)
}
================================================
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 <Component {...pageProps} />
}
================================================
FILE: packages/browser/examples/nextjs/pages/_error.js
================================================
function Error({ statusCode }) {
return (
<p>
{statusCode
? `An error ${statusCode} occurred on server`
: 'An error occurred on client'}
</p>
)
}
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 (
<ErrorBoundary>
<Layout home>
<Head>
<title>{siteTitle}</title>
</Head>
<section className={utilStyles.headingMd}>
<p>
Hello, I’m <strong>Shu</strong>. I’m a software engineer and a
translator (English/Japanese). You can contact me on{' '}
<a href="https://twitter.com/chibicode">Twitter</a>.
</p>
<p>
(This is a sample website - you’ll be building a site like this in{' '}
<a href="https://nextjs.org/learn">our Next.js tutorial</a>.)
</p>
</section>
<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
<h2 className={utilStyles.headingLg}>Blog</h2>
<ul className={utilStyles.list}>
{allPostsData.map(({ id, date, title }) => (
<li className={utilStyles.listItem} key={id}>
<Link href={`/posts/${id}`}>
<a>{title}</a>
</Link>
<br />
<small className={utilStyles.lightText}>
<Date dateString={date} />
</small>
</li>
))}
</ul>
</section>
<button
type="button"
onClick={() => {
throw new Error("Client Error");
}}
>
Throw error
</button>
</Layout>
</ErrorBoundary>
)
}
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 (
<Layout>
<Head>
<title>{postData.title}</title>
</Head>
<article>
<h1 className={utilStyles.headingXl}>{postData.title}</h1>
<div className={utilStyles.lightText}>
<Date dateString={postData.date} />
</div>
<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
</article>
</Layout>
)
}
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 <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;
```
Then you can use it as a regular component:
```html
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
```
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<INotice> {
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('<empty string>');
} 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<INotice> {
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 === '<anonymous>') {
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<IHttpResponse>;
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<IHttpResponse> {
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<IHttpResponse> => {
return request(req, api);
};
}
let rateLimitReset = 0;
function request(req: IHttpRequest, api: requestAPI): Promise<IHttpResponse> {
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<Response> {
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<INotice> {
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<INotice>;
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<T> = {
[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<T>(obj: T): Entries<T> {
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('<empty string>');
});
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 <anonymous> file', () => {
client.notify({ message: 'test', fileName: '<anonymous>' });
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('<empty string>');
});
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
================================================
<p align="center">
<img src="https://airbrake-github-assets.s3.amazonaws.com/brand/airbrake-full-logo.png" width="200">
</p>
# Official Airbrake Notifier for Node.js
[](https://github.com/airbrake/airbrake-js/actions?query=branch%3Amaster)
[](https://www.npmjs.com/package/@airbrake/node)
[](https://www.npmjs.com/package/@airbrake/node)
[](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/(.*)$': '<rootDir>/../$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).cat
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
SYMBOL INDEX (260 symbols across 58 files)
FILE: packages/browser/examples/nextjs/components/date.js
function Date (line 3) | function Date({ dateString }) {
FILE: packages/browser/examples/nextjs/components/error_boundary.js
class ErrorBoundary (line 4) | class ErrorBoundary extends React.Component {
method constructor (line 5) | constructor(props) {
method componentDidCatch (line 15) | componentDidCatch(error, info) {
method render (line 25) | render() {
FILE: packages/browser/examples/nextjs/components/layout.js
function Layout (line 10) | function Layout({ children, home }) {
FILE: packages/browser/examples/nextjs/lib/posts.js
function getSortedPostsData (line 9) | function getSortedPostsData() {
function getAllPostIds (line 39) | function getAllPostIds() {
function getPostData (line 50) | async function getPostData(id) {
FILE: packages/browser/examples/nextjs/pages/_app.js
function App (line 3) | function App({ Component, pageProps }) {
FILE: packages/browser/examples/nextjs/pages/_error.js
function Error (line 1) | function Error({ statusCode }) {
FILE: packages/browser/examples/nextjs/pages/index.js
function Home (line 9) | function Home({ allPostsData }) {
function getStaticProps (line 56) | async function getStaticProps() {
FILE: packages/browser/examples/nextjs/pages/posts/[id].js
function Post (line 7) | function Post({ postData }) {
function getStaticPaths (line 24) | async function getStaticPaths() {
function getStaticProps (line 33) | async function getStaticProps({ params }) {
FILE: packages/browser/rollup.config.js
function umd (line 14) | function umd(cfg) {
FILE: packages/browser/src/base_notifier.ts
class BaseNotifier (line 27) | class BaseNotifier {
method constructor (line 43) | constructor(opt: IOptions) {
method close (line 95) | close(): void {
method scope (line 101) | scope(): Scope {
method setActiveScope (line 105) | setActiveScope(scope: Scope) {
method addFilter (line 109) | addFilter(filter: Filter): void {
method addPerformanceFilter (line 113) | addPerformanceFilter(performanceFilter: PerformanceFilter) {
method notify (line 117) | notify(err: any): Promise<INotice> {
method handleFalseyError (line 153) | private handleFalseyError(err: any) {
method newNotice (line 165) | private newNotice(err: any): INotice {
method _sendNotice (line 179) | _sendNotice(notice: INotice): Promise<INotice> {
method wrap (line 208) | wrap(fn, props: string[] = []): IFuncWrapper {
method _wrapArguments (line 244) | _wrapArguments(args: any[]): any[] {
method _ignoreNextWindowError (line 254) | _ignoreNextWindowError() {}
method call (line 256) | call(fn, ..._args: any[]): any {
class Routes (line 262) | class Routes {
method constructor (line 268) | constructor(notifier: BaseNotifier) {
method start (line 275) | start(
method notify (line 295) | notify(req: RouteMetric): void {
class Queues (line 312) | class Queues {
method constructor (line 317) | constructor(notifier: BaseNotifier) {
method start (line 323) | start(queue: string): QueueMetric {
method notify (line 338) | notify(q: QueueMetric): void {
FILE: packages/browser/src/filter/angular_message.ts
function angularMessageFilter (line 13) | function angularMessageFilter(notice: INotice): INotice {
FILE: packages/browser/src/filter/debounce.ts
function makeDebounceFilter (line 4) | function makeDebounceFilter(): Filter {
FILE: packages/browser/src/filter/filter.ts
type Filter (line 3) | type Filter = (notice: INotice) => INotice | null;
FILE: packages/browser/src/filter/ignore_noise.ts
constant IGNORED_MESSAGES (line 3) | const IGNORED_MESSAGES = [
function ignoreNoiseFilter (line 9) | function ignoreNoiseFilter(notice: INotice): INotice | null {
FILE: packages/browser/src/filter/performance_filter.ts
type PerformanceFilter (line 3) | type PerformanceFilter = (metric: RouteMetric) => RouteMetric | null;
FILE: packages/browser/src/filter/uncaught_message.ts
function uncaughtMessageFilter (line 13) | function uncaughtMessageFilter(notice: INotice): INotice {
FILE: packages/browser/src/filter/window.ts
function windowFilter (line 3) | function windowFilter(notice: INotice): INotice {
FILE: packages/browser/src/func_wrapper.ts
type IFuncWrapper (line 1) | interface IFuncWrapper {
FILE: packages/browser/src/http_req/api.ts
type IHttpRequest (line 1) | interface IHttpRequest {
type IHttpResponse (line 9) | interface IHttpResponse {
type Requester (line 13) | type Requester = (req: IHttpRequest) => Promise<IHttpResponse>;
FILE: packages/browser/src/http_req/fetch.ts
function request (line 7) | function request(req: IHttpRequest): Promise<IHttpResponse> {
FILE: packages/browser/src/http_req/index.ts
function makeRequester (line 8) | function makeRequester(opts: IOptions): Requester {
FILE: packages/browser/src/http_req/node.ts
type requestAPI (line 6) | type requestAPI = request_lib.RequestAPI<
function makeRequester (line 12) | function makeRequester(api: requestAPI): Requester {
function request (line 20) | function request(req: IHttpRequest, api: requestAPI): Promise<IHttpRespo...
FILE: packages/browser/src/instrumentation/console.ts
constant CONSOLE_METHODS (line 4) | const CONSOLE_METHODS = ['debug', 'log', 'info', 'warn', 'error'];
function instrumentConsole (line 6) | function instrumentConsole(notifier: Notifier): void {
FILE: packages/browser/src/instrumentation/dom.ts
function instrumentDOM (line 5) | function instrumentDOM(notifier: Notifier) {
function makeEventHandler (line 29) | function makeEventHandler(notifier: Notifier): EventListener {
function elemName (line 48) | function elemName(elem: HTMLElement): string {
function classNameString (line 87) | function classNameString(name: any): string {
function elemPath (line 99) | function elemPath(elem: HTMLElement): string {
function getProp (line 123) | function getProp(obj: any, prop: string): any {
FILE: packages/browser/src/instrumentation/fetch.ts
function instrumentFetch (line 3) | function instrumentFetch(notifier: Notifier): void {
FILE: packages/browser/src/instrumentation/location.ts
function getCurrentLocation (line 6) | function getCurrentLocation(): string | null {
function instrumentLocation (line 10) | function instrumentLocation(notifier: Notifier): void {
function recordLocation (line 37) | function recordLocation(notifier: Notifier, url: string): void {
FILE: packages/browser/src/instrumentation/unhandledrejection.ts
function instrumentUnhandledrejection (line 3) | function instrumentUnhandledrejection(notifier: Notifier): void {
function onUnhandledrejection (line 12) | function onUnhandledrejection(e: any): void {
FILE: packages/browser/src/instrumentation/xhr.ts
type IXMLHttpRequestWithState (line 3) | interface IXMLHttpRequestWithState extends XMLHttpRequest {
function instrumentXHR (line 7) | function instrumentXHR(notifier: Notifier): void {
FILE: packages/browser/src/jsonify_notice.ts
constant FILTERED (line 3) | const FILTERED = '[Filtered]';
constant MAX_OBJ_LENGTH (line 4) | const MAX_OBJ_LENGTH = 128;
function jsonifyNotice (line 8) | function jsonifyNotice(
function scale (line 57) | function scale(num: number, level: number): number {
type ITruncatorOptions (line 61) | interface ITruncatorOptions {
class Truncator (line 67) | class Truncator {
method constructor (line 78) | constructor(opts: ITruncatorOptions) {
method truncate (line 89) | public truncate(value: any, key = '', depth = 0): any {
method getPath (line 156) | private getPath(value): string {
method truncateString (line 169) | private truncateString(s: string): string {
method truncateArray (line 176) | private truncateArray(arr: any[], depth = 0): any[] {
method truncateObject (line 191) | private truncateObject(obj: any, depth = 0): any {
method filterKey (line 216) | private filterKey(key: string, obj: any): boolean {
function truncate (line 229) | function truncate(value: any, opts: ITruncatorOptions = {}): any {
function getAttr (line 234) | function getAttr(obj: any, attr: string): any {
function objectType (line 243) | function objectType(obj: any): string {
function isInList (line 248) | function isInList(key: string, list: any[]): boolean {
FILE: packages/browser/src/metrics.ts
type IMetric (line 1) | interface IMetric {
class Span (line 8) | class Span {
method constructor (line 18) | constructor(metric: IMetric, name: string, startTime?: Date) {
method end (line 25) | end(endTime?: Date) {
method _pause (line 33) | _pause() {
method _resume (line 42) | _resume() {
method _paused (line 49) | _paused() {
class BaseMetric (line 54) | class BaseMetric implements IMetric {
method constructor (line 61) | constructor() {
method end (line 65) | end(endTime?: Date): void {
method isRecording (line 71) | isRecording(): boolean {
method startSpan (line 75) | startSpan(name: string, startTime?: Date): void {
method endSpan (line 85) | endSpan(name: string, endTime?: Date): void {
method _incGroup (line 100) | _incGroup(name: string, ms: number): void {
method _duration (line 104) | _duration(): number {
class NoopMetric (line 112) | class NoopMetric implements IMetric {
method isRecording (line 113) | isRecording(): boolean {
method startSpan (line 116) | startSpan(_name: string, _startTime?: Date): void {}
method endSpan (line 117) | endSpan(_name: string, _startTime?: Date): void {}
method _incGroup (line 118) | _incGroup(_name: string, _ms: number): void {}
FILE: packages/browser/src/notice.ts
type INoticeFrame (line 1) | interface INoticeFrame {
type INoticeError (line 8) | interface INoticeError {
type INotice (line 14) | interface INotice {
FILE: packages/browser/src/notifier.ts
type ITodo (line 13) | interface ITodo {
class Notifier (line 19) | class Notifier extends BaseNotifier {
method constructor (line 26) | constructor(opt: IOptions) {
method _instrument (line 50) | _instrument(opt: IInstrumentationOptions = {}) {
method notify (line 88) | public notify(err: any): Promise<INotice> {
method onOnline (line 111) | protected onOnline(): void {
method onOffline (line 122) | protected onOffline(): void {
method onerror (line 126) | onerror(
method _ignoreNextWindowError (line 166) | _ignoreNextWindowError(): void {
function isDevEnv (line 172) | function isDevEnv(env: any): boolean {
function enabled (line 176) | function enabled(v: undefined | boolean): boolean {
FILE: packages/browser/src/options.ts
type Reporter (line 6) | type Reporter = (notice: INotice) => Promise<INotice>;
type IInstrumentationOptions (line 8) | interface IInstrumentationOptions {
type IOptions (line 17) | interface IOptions {
FILE: packages/browser/src/processor/esp.ts
type IStackFrame (line 7) | interface IStackFrame {
type IError (line 14) | interface IError extends Error, IStackFrame {
function parse (line 18) | function parse(err: IError): IStackFrame[] {
function espProcessor (line 34) | function espProcessor(err: IError): INoticeError {
FILE: packages/browser/src/processor/processor.ts
type Processor (line 3) | type Processor = (err: Error) => INoticeError;
FILE: packages/browser/src/queries.ts
constant FLUSH_INTERVAL (line 5) | const FLUSH_INTERVAL = 15000;
type IQueryKey (line 7) | interface IQueryKey {
class QueryInfo (line 17) | class QueryInfo {
method constructor (line 27) | constructor(query = '') {
method _duration (line 31) | _duration(): number {
class QueriesStats (line 39) | class QueriesStats {
method constructor (line 47) | constructor(opt: IOptions) {
method start (line 53) | start(query = ''): QueryInfo {
method notify (line 57) | notify(q: QueryInfo): void {
method _flush (line 103) | _flush(): void {
FILE: packages/browser/src/queues.ts
constant FLUSH_INTERVAL (line 6) | const FLUSH_INTERVAL = 15000;
type IQueueKey (line 8) | interface IQueueKey {
class QueueMetric (line 13) | class QueueMetric extends BaseMetric {
method constructor (line 16) | constructor(queue: string) {
class QueuesStats (line 23) | class QueuesStats {
method constructor (line 31) | constructor(opt: IOptions) {
method notify (line 37) | notify(q: QueueMetric): void {
method _flush (line 81) | _flush(): void {
FILE: packages/browser/src/remote_settings.ts
constant API_VER (line 6) | const API_VER = '2020-06-18';
constant DEFAULT_INTERVAL (line 9) | const DEFAULT_INTERVAL = 600000;
constant NOTIFIER_INFO (line 11) | const NOTIFIER_INFO = {
constant ERROR_SETTING (line 24) | const ERROR_SETTING = 'errors';
constant APM_SETTING (line 25) | const APM_SETTING = 'apm';
type IRemoteConfig (line 27) | interface IRemoteConfig {
type IRemoteConfigSetting (line 35) | interface IRemoteConfigSetting {
type Entries (line 41) | type Entries<T> = {
class RemoteSettings (line 45) | class RemoteSettings {
method constructor (line 52) | constructor(opt: IOptions) {
method poll (line 68) | poll(): any {
method _doRequest (line 80) | _doRequest(): void {
method _requestParams (line 96) | _requestParams(opt: IOptions): any {
method _pollUrl (line 107) | _pollUrl(opt: IOptions): string {
method _processErrorNotifications (line 118) | _processErrorNotifications(data: SettingsData): void {
method _processPerformanceStats (line 125) | _processPerformanceStats(data: SettingsData): void {
method _entries (line 134) | _entries<T>(obj: T): Entries<T> {
class SettingsData (line 145) | class SettingsData {
method constructor (line 149) | constructor(projectId: number, data: IRemoteConfig) {
method merge (line 154) | merge(other: IRemoteConfig) {
method configRoute (line 158) | configRoute(remoteConfigHost: string): string {
method errorNotifications (line 173) | errorNotifications(): boolean {
method performanceStats (line 182) | performanceStats(): boolean {
method errorHost (line 191) | errorHost(): string {
method apmHost (line 200) | apmHost(): string {
method _findSetting (line 209) | _findSetting(name: string): IRemoteConfigSetting {
FILE: packages/browser/src/routes.ts
constant FLUSH_INTERVAL (line 6) | const FLUSH_INTERVAL = 15000;
type IRouteKey (line 8) | interface IRouteKey {
type IBreakdownKey (line 15) | interface IBreakdownKey {
class RouteMetric (line 22) | class RouteMetric extends BaseMetric {
method constructor (line 28) | constructor(method = '', route = '', statusCode = 0, contentType = '') {
class RoutesStats (line 38) | class RoutesStats {
method constructor (line 46) | constructor(opt: IOptions) {
method notify (line 52) | notify(req: RouteMetric): void {
method _flush (line 92) | _flush(): void {
class RoutesBreakdowns (line 132) | class RoutesBreakdowns {
method constructor (line 140) | constructor(opt: IOptions) {
method notify (line 146) | notify(req: RouteMetric): void {
method _flush (line 198) | _flush(): void {
method _responseType (line 237) | _responseType(req: RouteMetric): string {
FILE: packages/browser/src/scope.ts
type IHistoryRecord (line 3) | interface IHistoryRecord {
type IMap (line 9) | interface IMap {
class Scope (line 13) | class Scope {
method clone (line 24) | clone(): Scope {
method setContext (line 31) | setContext(context: IMap) {
method context (line 35) | context(): IMap {
method pushHistory (line 43) | pushHistory(state: IHistoryRecord): void {
method _isDupState (line 64) | private _isDupState(state): boolean {
method routeMetric (line 79) | routeMetric(): IMetric {
method setRouteMetric (line 83) | setRouteMetric(metric: IMetric) {
method queueMetric (line 87) | queueMetric(): IMetric {
method setQueueMetric (line 91) | setQueueMetric(metric: IMetric) {
FILE: packages/browser/src/tdshared.ts
type ICentroid (line 9) | interface ICentroid {
type ICentroids (line 14) | interface ICentroids {
type ITDigest (line 18) | interface ITDigest {
type ITDigestCentroids (line 25) | interface ITDigestCentroids {
class TDigestStat (line 30) | class TDigestStat {
method add (line 36) | add(ms: number) {
method toJSON (line 48) | toJSON() {
class TDigestStatGroups (line 58) | class TDigestStatGroups extends TDigestStat {
method addGroups (line 61) | addGroups(totalMs: number, groups: { [key: string]: number }) {
method addGroup (line 70) | addGroup(name: string, ms: number) {
method toJSON (line 79) | toJSON() {
function tdigestCentroids (line 90) | function tdigestCentroids(td: ITDigest): ITDigestCentroids {
FILE: packages/browser/src/version.ts
constant NOTIFIER_NAME (line 1) | const NOTIFIER_NAME = 'airbrake-js/browser';
constant NOTIFIER_VERSION (line 2) | const NOTIFIER_VERSION = '2.1.9';
constant NOTIFIER_URL (line 3) | const NOTIFIER_URL =
FILE: packages/browser/tests/client.test.js
function test (line 43) | function test(keysBlocklist) {
FILE: packages/browser/tests/historian.test.js
class Location (line 7) | class Location {
method constructor (line 8) | constructor(s) {
method toString (line 12) | toString() {
FILE: packages/browser/tests/processor/stacktracejs.test.js
function throwTestError (line 8) | function throwTestError() {
FILE: packages/node/examples/express/app.js
function main (line 7) | async function main() {
FILE: packages/node/src/filter/node.ts
function nodeFilter (line 6) | function nodeFilter(notice: INotice): INotice {
FILE: packages/node/src/instrumentation/debug.ts
function patch (line 3) | function patch(createDebug, airbrake: Notifier): void {
FILE: packages/node/src/instrumentation/express.ts
function makeMiddleware (line 3) | function makeMiddleware(airbrake: Notifier) {
function makeErrorHandler (line 25) | function makeErrorHandler(airbrake: Notifier) {
FILE: packages/node/src/instrumentation/http.ts
constant SPAN_NAME (line 3) | const SPAN_NAME = 'http';
function patch (line 5) | function patch(http, airbrake: Notifier): void {
function wrapRequest (line 14) | function wrapRequest(origFn, airbrake: Notifier) {
FILE: packages/node/src/instrumentation/https.ts
function patch (line 5) | function patch(https, airbrake: Notifier): void {
FILE: packages/node/src/instrumentation/mysql.ts
constant SPAN_NAME (line 4) | const SPAN_NAME = 'sql';
function patch (line 6) | function patch(mysql, airbrake: Notifier): void {
function wrapCreatePool (line 24) | function wrapCreatePool(origFn, airbrake: Notifier) {
function wrapGetConnection (line 32) | function wrapGetConnection(origFn, airbrake: Notifier) {
function wrapConnection (line 47) | function wrapConnection(conn, airbrake: Notifier): void {
FILE: packages/node/src/instrumentation/mysql2.ts
constant SPAN_NAME (line 4) | const SPAN_NAME = 'sql';
function patch (line 6) | function patch(mysql2, airbrake: Notifier): void {
function wrapQuery (line 12) | function wrapQuery(origQuery, airbrake: Notifier) {
FILE: packages/node/src/instrumentation/pg.ts
constant SPAN_NAME (line 4) | const SPAN_NAME = 'sql';
function patch (line 6) | function patch(pg, airbrake: Notifier): void {
function patchClient (line 23) | function patchClient(Client, airbrake: Notifier): void {
FILE: packages/node/src/instrumentation/redis.ts
constant SPAN_NAME (line 3) | const SPAN_NAME = 'redis';
function patch (line 5) | function patch(redis, airbrake: Notifier): void {
FILE: packages/node/src/notifier.ts
class Notifier (line 5) | class Notifier extends BaseNotifier {
method constructor (line 11) | constructor(opt: IOptions) {
method scope (line 60) | scope(): Scope {
method setActiveScope (line 70) | setActiveScope(scope: Scope) {
method notify (line 74) | notify(err: any): Promise<INotice> {
method flush (line 81) | async flush(timeout = 3000): Promise<boolean> {
method _instrument (line 109) | _instrument() {
FILE: packages/node/src/scope.ts
class ScopeManager (line 6) | class ScopeManager {
method constructor (line 10) | constructor() {
method setActive (line 20) | setActive(scope: Scope) {
method active (line 25) | active(): Scope {
method _init (line 30) | _init(aid: number) {
method _destroy (line 34) | _destroy(aid: number) {
FILE: packages/node/src/version.ts
constant NOTIFIER_NAME (line 1) | const NOTIFIER_NAME = 'airbrake-js/node';
constant NOTIFIER_VERSION (line 2) | const NOTIFIER_VERSION = '2.1.9';
constant NOTIFIER_URL (line 3) | const NOTIFIER_URL =
Condensed preview — 125 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (209K chars).
[
{
"path": ".editorconfig",
"chars": 24,
"preview": "[**.ts]\nindent_size = 2\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 1556,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve Airbrake\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n# 🐞 bu"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 641,
"preview": "---\nname: Feature request\nabout: Suggest a feature for Airbrake\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n# 🚀 fe"
},
{
"path": ".github/dependabot.yml",
"chars": 138,
"preview": "version: 2\n\nupdates:\n- package-ecosystem: npm\n directory: \"/\"\n schedule:\n interval: daily\n time: \"19:00\"\n tim"
},
{
"path": ".github/workflows/ci.yml",
"chars": 469,
"preview": "name: CI\n\non: [push]\n\npermissions:\n contents: read\n\njobs:\n lint_and_test:\n runs-on: ubuntu-latest\n\n strategy:\n "
},
{
"path": ".gitignore",
"chars": 134,
"preview": "node_modules\n.rpt2_cache\npackages/*/yarn.lock\npackages/*/package-lock.json\npackages/*/dist\npackages/*/esm\npackages/brows"
},
{
"path": ".prettierrc.toml",
"chars": 64,
"preview": "singleQuote = true\ntrailingComma = \"es5\"\narrowParens = \"always\"\n"
},
{
"path": "CHANGELOG.md",
"chars": 8223,
"preview": "# Airbrake JS Changelog\n\n### master\n\n### [2.1.9] (March 6, 2025)\n\n#### browser\n\n- Added the `keysAllowlist` option, whic"
},
{
"path": "CONTRIBUTING.md",
"chars": 1037,
"preview": "# Contributing\n\nBug fixes and improvements may be submitted in the form of pull requests.\n\n## Development Setup\n\nYou wil"
},
{
"path": "LICENSE.md",
"chars": 1084,
"preview": "# MIT License\n\nCopyright © 2022 Airbrake Technologies, Inc.\n\nPermission is hereby granted, free of charge, to any person"
},
{
"path": "README.md",
"chars": 608,
"preview": "<p align=\"center\">\n <img src=\"https://airbrake-github-assets.s3.amazonaws.com/brand/airbrake-full-logo.png\" width=\"200\""
},
{
"path": "lerna.json",
"chars": 72,
"preview": "{\n \"version\": \"2.1.9\",\n \"npmClient\": \"yarn\",\n \"useWorkspaces\": true\n}"
},
{
"path": "package.json",
"chars": 521,
"preview": "{\n \"name\": \"airbrake\",\n \"private\": true,\n \"devDependencies\": {\n \"concurrently\": \"^7.6.0\",\n \"lerna\": \"^6.0.3\",\n "
},
{
"path": "packages/browser/LICENSE",
"chars": 1084,
"preview": "MIT License\n\nCopyright (c) 2020 Airbrake Technologies, Inc.\n\nPermission is hereby granted, free of charge, to any person"
},
{
"path": "packages/browser/README.md",
"chars": 9133,
"preview": "<p align=\"center\">\n <img src=\"https://airbrake-github-assets.s3.amazonaws.com/brand/airbrake-full-logo.png\" width=\"200\""
},
{
"path": "packages/browser/babel.config.js",
"chars": 92,
"preview": "module.exports = {\n presets: [['@babel/preset-env', { targets: { node: 'current' } }]],\n};\n"
},
{
"path": "packages/browser/examples/angular/README.md",
"chars": 1585,
"preview": "# Usage with Angular\n\n### Create an error handler\nThe first step is to create an error handler with a `Notifier`\ninitial"
},
{
"path": "packages/browser/examples/angularjs/README.md",
"chars": 1252,
"preview": "# Usage with AngularJS\n\nIntegration with AngularJS is as simple as adding an [$exceptionHandler][1]:\n\n```js\n// app.js\nco"
},
{
"path": "packages/browser/examples/extjs/README.md",
"chars": 927,
"preview": "# Usage with Ext JS\n\n### Install the `@airbrake/browser` package\n\n```sh\nnpm i @airbrake/browser\n```\n\n### Make the packag"
},
{
"path": "packages/browser/examples/legacy/README.md",
"chars": 161,
"preview": "# Usage with legacy applications\n\nThis example loads @airbrake/browser using a `script` tag via the jsdelivr CDN.\nOpen `"
},
{
"path": "packages/browser/examples/legacy/app.js",
"chars": 601,
"preview": "var airbrake = new Airbrake.Notifier({\n projectId: 1,\n projectKey: 'FIXME',\n});\n\n$(function() {\n $('#send_error').cli"
},
{
"path": "packages/browser/examples/legacy/index.html",
"chars": 474,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, in"
},
{
"path": "packages/browser/examples/nextjs/.gitignore",
"chars": 289,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "packages/browser/examples/nextjs/README.md",
"chars": 2592,
"preview": "# Usage with Next.js\nThis is a sample application that can be found at\n[Learn Next.js](https://nextjs.org/learn). It has"
},
{
"path": "packages/browser/examples/nextjs/components/date.js",
"chars": 205,
"preview": "import { parseISO, format } from 'date-fns'\n\nexport default function Date({ dateString }) {\n const date = parseISO(date"
},
{
"path": "packages/browser/examples/nextjs/components/error_boundary.js",
"chars": 849,
"preview": "import React from 'react';\nimport { Notifier } from '@airbrake/browser';\n\nclass ErrorBoundary extends React.Component {\n"
},
{
"path": "packages/browser/examples/nextjs/components/layout.js",
"chars": 2155,
"preview": "import Head from 'next/head'\nimport Image from 'next/image'\nimport styles from './layout.module.css'\nimport utilStyles f"
},
{
"path": "packages/browser/examples/nextjs/components/layout.module.css",
"chars": 196,
"preview": ".container {\n max-width: 36rem;\n padding: 0 1rem;\n margin: 3rem auto 6rem;\n}\n\n.header {\n display: flex;\n flex-direc"
},
{
"path": "packages/browser/examples/nextjs/lib/posts.js",
"chars": 1751,
"preview": "import fs from 'fs'\nimport path from 'path'\nimport matter from 'gray-matter'\nimport { remark } from 'remark'\nimport html"
},
{
"path": "packages/browser/examples/nextjs/package.json",
"chars": 392,
"preview": "{\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev\",\n \"build\": \"next build\",\n \"start\": \"next start\"\n },\n \""
},
{
"path": "packages/browser/examples/nextjs/pages/_app.js",
"chars": 127,
"preview": "import '../styles/global.css'\n\nexport default function App({ Component, pageProps }) {\n return <Component {...pageProps"
},
{
"path": "packages/browser/examples/nextjs/pages/_error.js",
"chars": 662,
"preview": "function Error({ statusCode }) {\n return (\n <p>\n {statusCode\n ? `An error ${statusCode} occurred on serv"
},
{
"path": "packages/browser/examples/nextjs/pages/api/hello.js",
"chars": 75,
"preview": "export default (req, res) => {\n res.status(200).json({ text: 'Hello' })\n}\n"
},
{
"path": "packages/browser/examples/nextjs/pages/index.js",
"chars": 1957,
"preview": "import Head from 'next/head'\nimport Layout, { siteTitle } from '../components/layout'\nimport utilStyles from '../styles/"
},
{
"path": "packages/browser/examples/nextjs/pages/posts/[id].js",
"chars": 979,
"preview": "import Layout from '../../components/layout'\nimport { getAllPostIds, getPostData } from '../../lib/posts'\nimport Head fr"
},
{
"path": "packages/browser/examples/nextjs/posts/pre-rendering.md",
"chars": 686,
"preview": "---\ntitle: \"Two Forms of Pre-rendering\"\ndate: \"2020-01-01\"\n---\n\nNext.js has two forms of pre-rendering: **Static Generat"
},
{
"path": "packages/browser/examples/nextjs/posts/ssg-ssr.md",
"chars": 1036,
"preview": "---\ntitle: \"When to Use Static Generation v.s. Server-side Rendering\"\ndate: \"2020-01-02\"\n---\n\nWe recommend using **Stati"
},
{
"path": "packages/browser/examples/nextjs/styles/global.css",
"chars": 403,
"preview": "html,\nbody {\n padding: 0;\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,\n "
},
{
"path": "packages/browser/examples/nextjs/styles/utils.module.css",
"chars": 621,
"preview": ".heading2Xl {\n font-size: 2.5rem;\n line-height: 1.2;\n font-weight: 800;\n letter-spacing: -0.05rem;\n margin: 1rem 0;"
},
{
"path": "packages/browser/examples/rails/README.md",
"chars": 1571,
"preview": "# Usage with Ruby on Rails\n\n#### Option 1 - Asset pipeline\n\nCopy the latest compiled UMD package bundle from\n[https://un"
},
{
"path": "packages/browser/examples/react/README.md",
"chars": 1214,
"preview": "# Usage with React\n\nTo report errors from a React app, you'll need to set up and use an\n[`ErrorBoundary` component](http"
},
{
"path": "packages/browser/examples/redux/README.md",
"chars": 1302,
"preview": "# Usage with Redux\n\n#### 1. Add dependencies\n```bash\nnpm install @airbrake/browser redux-airbrake --save\n```\n\n#### 2. Im"
},
{
"path": "packages/browser/examples/svelte/README.md",
"chars": 3022,
"preview": "# Usage with Svelte\n\nIntegration with Svelte is as simple as adding `handleError` hooks (Server or Client):\n\n- For serve"
},
{
"path": "packages/browser/examples/vuejs/README.md",
"chars": 1091,
"preview": "Usage with Vue.js\n==================\n\n### Vue 2\n\nYou can start reporting errors from your Vue 2 app by configuring an\n[`"
},
{
"path": "packages/browser/jest.config.js",
"chars": 173,
"preview": "module.exports = {\n transform: {\n '^.+\\\\.jsx?$': 'babel-jest',\n '^.+\\\\.tsx?$': 'ts-jest',\n },\n testEnvironment:"
},
{
"path": "packages/browser/package.json",
"chars": 1959,
"preview": "{\n \"name\": \"@airbrake/browser\",\n \"version\": \"2.1.9\",\n \"description\": \"Official Airbrake notifier for browsers\",\n \"au"
},
{
"path": "packages/browser/rollup.config.js",
"chars": 738,
"preview": "import commonjs from '@rollup/plugin-commonjs';\nimport resolve from '@rollup/plugin-node-resolve';\nimport typescript fro"
},
{
"path": "packages/browser/src/base_notifier.ts",
"chars": 9063,
"preview": "import Promise from 'promise-polyfill';\n\nimport { IFuncWrapper } from './func_wrapper';\nimport { jsonifyNotice } from '."
},
{
"path": "packages/browser/src/filter/angular_message.ts",
"chars": 458,
"preview": "import { INotice } from '../notice';\n\nlet re = new RegExp(\n [\n '^',\n '\\\\[(\\\\$.+)\\\\]', // type\n '\\\\s',\n '([\\"
},
{
"path": "packages/browser/src/filter/debounce.ts",
"chars": 495,
"preview": "import { INotice } from '../notice';\nimport { Filter } from './filter';\n\nexport function makeDebounceFilter(): Filter {\n"
},
{
"path": "packages/browser/src/filter/filter.ts",
"chars": 96,
"preview": "import { INotice } from '../notice';\n\nexport type Filter = (notice: INotice) => INotice | null;\n"
},
{
"path": "packages/browser/src/filter/ignore_noise.ts",
"chars": 498,
"preview": "import { INotice } from '../notice';\n\nconst IGNORED_MESSAGES = [\n 'Script error',\n 'Script error.',\n 'InvalidAccessEr"
},
{
"path": "packages/browser/src/filter/performance_filter.ts",
"chars": 119,
"preview": "import { RouteMetric } from '../routes';\n\nexport type PerformanceFilter = (metric: RouteMetric) => RouteMetric | null;\n"
},
{
"path": "packages/browser/src/filter/uncaught_message.ts",
"chars": 463,
"preview": "import { INotice } from '../notice';\n\nlet re = new RegExp(\n [\n '^',\n 'Uncaught\\\\s',\n '(.+?)', // type\n ':\\\\"
},
{
"path": "packages/browser/src/filter/window.ts",
"chars": 485,
"preview": "import { INotice } from '../notice';\n\nexport function windowFilter(notice: INotice): INotice {\n if (window.navigator &&"
},
{
"path": "packages/browser/src/func_wrapper.ts",
"chars": 88,
"preview": "export interface IFuncWrapper {\n (): any;\n inner: () => any;\n _airbrake?: boolean;\n}\n"
},
{
"path": "packages/browser/src/http_req/api.ts",
"chars": 418,
"preview": "export interface IHttpRequest {\n method: string;\n url: string;\n body?: string;\n timeout?: number;\n headers?: any;\n}"
},
{
"path": "packages/browser/src/http_req/fetch.ts",
"chars": 1500,
"preview": "import fetch from 'cross-fetch';\nimport Promise from 'promise-polyfill';\nimport { errors, IHttpRequest, IHttpResponse } "
},
{
"path": "packages/browser/src/http_req/index.ts",
"chars": 364,
"preview": "import { IOptions } from '../options';\nimport { Requester } from './api';\nimport { request as fetchRequest } from './fet"
},
{
"path": "packages/browser/src/http_req/node.ts",
"chars": 2799,
"preview": "import * as request_lib from 'request';\nimport Promise from 'promise-polyfill';\n\nimport { errors, IHttpRequest, IHttpRes"
},
{
"path": "packages/browser/src/index.ts",
"chars": 233,
"preview": "export { Notifier } from './notifier';\nexport { BaseNotifier } from './base_notifier';\nexport { INotice } from './notice"
},
{
"path": "packages/browser/src/instrumentation/console.ts",
"chars": 642,
"preview": "import { IFuncWrapper } from '../func_wrapper';\nimport { Notifier } from '../notifier';\n\nconst CONSOLE_METHODS = ['debug"
},
{
"path": "packages/browser/src/instrumentation/dom.ts",
"chars": 2708,
"preview": "import { Notifier } from '../notifier';\n\nconst elemAttrs = ['type', 'name', 'src'];\n\nexport function instrumentDOM(notif"
},
{
"path": "packages/browser/src/instrumentation/fetch.ts",
"chars": 1184,
"preview": "import { Notifier } from '../notifier';\n\nexport function instrumentFetch(notifier: Notifier): void {\n // tslint:disable"
},
{
"path": "packages/browser/src/instrumentation/location.ts",
"chars": 1323,
"preview": "import { Notifier } from '../notifier';\n\nlet lastLocation = '';\n\n// In some environments (i.e. Cypress) document.locatio"
},
{
"path": "packages/browser/src/instrumentation/unhandledrejection.ts",
"chars": 1029,
"preview": "import { Notifier } from '../notifier';\n\nexport function instrumentUnhandledrejection(notifier: Notifier): void {\n cons"
},
{
"path": "packages/browser/src/instrumentation/xhr.ts",
"chars": 1333,
"preview": "import { Notifier } from '../notifier';\n\ninterface IXMLHttpRequestWithState extends XMLHttpRequest {\n __state: any;\n}\n\n"
},
{
"path": "packages/browser/src/jsonify_notice.ts",
"chars": 6040,
"preview": "import { INotice } from './notice';\n\nconst FILTERED = '[Filtered]';\nconst MAX_OBJ_LENGTH = 128;\n\n// jsonifyNotice serial"
},
{
"path": "packages/browser/src/metrics.ts",
"chars": 2430,
"preview": "export interface IMetric {\n isRecording(): boolean;\n startSpan(name: string, startTime?: Date): void;\n endSpan(name: "
},
{
"path": "packages/browser/src/notice.ts",
"chars": 380,
"preview": "export interface INoticeFrame {\n function: string;\n file: string;\n line: number;\n column: number;\n}\n\nexport interfac"
},
{
"path": "packages/browser/src/notifier.ts",
"chars": 4305,
"preview": "import Promise from 'promise-polyfill';\nimport { BaseNotifier } from './base_notifier';\nimport { windowFilter } from './"
},
{
"path": "packages/browser/src/options.ts",
"chars": 906,
"preview": "import * as request from 'request';\n\nimport { INotice } from './notice';\nimport { Processor } from './processor/processo"
},
{
"path": "packages/browser/src/processor/esp.ts",
"chars": 1614,
"preview": "import { INoticeError, INoticeFrame } from '../notice';\n\nimport ErrorStackParser from 'error-stack-parser';\n\nconst hasCo"
},
{
"path": "packages/browser/src/processor/processor.ts",
"chars": 97,
"preview": "import { INoticeError } from '../notice';\n\nexport type Processor = (err: Error) => INoticeError;\n"
},
{
"path": "packages/browser/src/queries.ts",
"chars": 2695,
"preview": "import { makeRequester, Requester } from './http_req';\nimport { IOptions } from './options';\nimport { hasTdigest, TDiges"
},
{
"path": "packages/browser/src/queues.ts",
"chars": 2365,
"preview": "import { makeRequester, Requester } from './http_req';\nimport { BaseMetric } from './metrics';\nimport { IOptions } from "
},
{
"path": "packages/browser/src/remote_settings.ts",
"chars": 5194,
"preview": "import { makeRequester, Requester } from './http_req';\nimport { IOptions } from './options';\nimport { NOTIFIER_NAME, NOT"
},
{
"path": "packages/browser/src/routes.ts",
"chars": 5106,
"preview": "import { makeRequester, Requester } from './http_req';\nimport { BaseMetric } from './metrics';\nimport { IOptions } from "
},
{
"path": "packages/browser/src/scope.ts",
"chars": 1911,
"preview": "import { IMetric, NoopMetric } from './metrics';\n\ninterface IHistoryRecord {\n type: string;\n date?: Date;\n [key: stri"
},
{
"path": "packages/browser/src/tdshared.ts",
"chars": 1808,
"preview": "let tdigest;\nexport let hasTdigest = false;\n\ntry {\n tdigest = require('tdigest');\n hasTdigest = true;\n} catch (err) {}"
},
{
"path": "packages/browser/src/version.ts",
"chars": 195,
"preview": "export const NOTIFIER_NAME = 'airbrake-js/browser';\nexport const NOTIFIER_VERSION = '2.1.9';\nexport const NOTIFIER_URL ="
},
{
"path": "packages/browser/tests/client.test.js",
"chars": 19064,
"preview": "import { Notifier } from '../src/notifier';\n\ndescribe('Notifier config', () => {\n const reporter = jest.fn(() => Promis"
},
{
"path": "packages/browser/tests/historian.test.js",
"chars": 6179,
"preview": "let { fetch, Request } = require('cross-fetch');\n\nwindow.fetch = fetch;\n\nimport { Notifier } from '../src/notifier';\n\ncl"
},
{
"path": "packages/browser/tests/jsonify_notice.test.js",
"chars": 4736,
"preview": "import { jsonifyNotice } from '../src/jsonify_notice';\n\ndescribe('jsonify_notice', () => {\n const maxLength = 30000;\n\n "
},
{
"path": "packages/browser/tests/processor/stacktracejs.test.js",
"chars": 1360,
"preview": "import { INoticeError } from '../../src/notice';\nimport { espProcessor } from '../../src/processor/esp';\n\ndescribe('stac"
},
{
"path": "packages/browser/tests/remote_settings.test.js",
"chars": 5672,
"preview": "import { SettingsData } from '../src/remote_settings';\n\ndescribe('SettingsData', () => {\n describe('merge', () => {\n "
},
{
"path": "packages/browser/tests/truncate.test.js",
"chars": 3815,
"preview": "import { truncate } from '../src/jsonify_notice';\n\ndescribe('truncate', () => {\n it('works', () => {\n /* tslint:disa"
},
{
"path": "packages/browser/tsconfig.cjs.json",
"chars": 110,
"preview": "{\n \"extends\": \"../../tsconfig.json\",\n \"compilerOptions\": {\n \"outDir\": \"dist\"\n },\n \"include\": [\"src\"]\n}\n"
},
{
"path": "packages/browser/tsconfig.esm.json",
"chars": 113,
"preview": "{\n \"extends\": \"../../tsconfig.esm.json\",\n \"compilerOptions\": {\n \"outDir\": \"esm\"\n },\n \"include\": [\"src\"]\n}\n"
},
{
"path": "packages/browser/tsconfig.json",
"chars": 117,
"preview": "{\n \"extends\": \"./tsconfig.cjs.json\",\n \"compilerOptions\": {\n \"rootDir\": \".\",\n },\n \"include\": [\"src\", \"test\"]\n}\n"
},
{
"path": "packages/browser/tsconfig.umd.json",
"chars": 143,
"preview": "{\n \"extends\": \"../../tsconfig.json\",\n \"compilerOptions\": {\n \"declaration\": false,\n \"declarationMap\": false,\n "
},
{
"path": "packages/browser/tslint.json",
"chars": 39,
"preview": "{\n \"extends\": [\"../../tslint.json\"]\n}\n"
},
{
"path": "packages/node/LICENSE",
"chars": 1084,
"preview": "MIT License\n\nCopyright (c) 2020 Airbrake Technologies, Inc.\n\nPermission is hereby granted, free of charge, to any person"
},
{
"path": "packages/node/README.md",
"chars": 5854,
"preview": "<p align=\"center\">\n <img src=\"https://airbrake-github-assets.s3.amazonaws.com/brand/airbrake-full-logo.png\" width=\"200\""
},
{
"path": "packages/node/babel.config.js",
"chars": 92,
"preview": "module.exports = {\n presets: [['@babel/preset-env', { targets: { node: 'current' } }]],\n};\n"
},
{
"path": "packages/node/examples/express/README.md",
"chars": 2425,
"preview": "# Using Airbrake with Express.js\n\nThis example Node.js application uses Express.js and sets up Airbrake to report\nerrors"
},
{
"path": "packages/node/examples/express/app.js",
"chars": 1176,
"preview": "const express = require('express');\nconst pg = require('pg');\n\nconst Airbrake = require('@airbrake/node');\nconst airbrak"
},
{
"path": "packages/node/examples/express/package.json",
"chars": 134,
"preview": "{\n \"name\": \"airbrake-example\",\n \"dependencies\": {\n \"@airbrake/node\": \"^2.1.9\",\n \"express\": \"^4.17.1\",\n \"pg\": "
},
{
"path": "packages/node/examples/nodejs/README.md",
"chars": 1931,
"preview": "# Using Airbrake with Node.js\n\n#### 1. Install the package\n```shell\nnpm install @airbrake/node\n```\n\n#### 2. Include @air"
},
{
"path": "packages/node/examples/nodejs/app.js",
"chars": 555,
"preview": "const http = require('http');\nconst Airbrake = require('@airbrake/node');\n\nnew Airbrake.Notifier({\n projectId: process."
},
{
"path": "packages/node/examples/nodejs/package.json",
"chars": 88,
"preview": "{\n \"name\": \"airbrake-example\",\n \"dependencies\": {\n \"@airbrake/node\": \"^2.1.9\"\n }\n}"
},
{
"path": "packages/node/jest.config.js",
"chars": 246,
"preview": "module.exports = {\n transform: {\n '^.+\\\\.jsx?$': 'babel-jest',\n '^.+\\\\.tsx?$': 'ts-jest',\n },\n testEnvironment:"
},
{
"path": "packages/node/package.json",
"chars": 1566,
"preview": "{\n \"name\": \"@airbrake/node\",\n \"version\": \"2.1.9\",\n \"description\": \"Official Airbrake notifier for Node.js\",\n \"author"
},
{
"path": "packages/node/src/filter/node.ts",
"chars": 1141,
"preview": "import { INotice } from '@airbrake/browser';\nimport { NOTIFIER_NAME, NOTIFIER_VERSION, NOTIFIER_URL } from '../version';"
},
{
"path": "packages/node/src/index.ts",
"chars": 39,
"preview": "export { Notifier } from './notifier';\n"
},
{
"path": "packages/node/src/instrumentation/debug.ts",
"chars": 454,
"preview": "import { Notifier } from '../notifier';\n\nexport function patch(createDebug, airbrake: Notifier): void {\n const oldInit "
},
{
"path": "packages/node/src/instrumentation/express.ts",
"chars": 1505,
"preview": "import { Notifier } from '../notifier';\n\nexport function makeMiddleware(airbrake: Notifier) {\n return function airbrake"
},
{
"path": "packages/node/src/instrumentation/http.ts",
"chars": 782,
"preview": "import { Notifier } from '../notifier';\n\nconst SPAN_NAME = 'http';\n\nexport function patch(http, airbrake: Notifier): voi"
},
{
"path": "packages/node/src/instrumentation/https.ts",
"chars": 297,
"preview": "import { Notifier } from '../notifier';\n\nimport { wrapRequest } from './http';\n\nexport function patch(https, airbrake: N"
},
{
"path": "packages/node/src/instrumentation/mysql.ts",
"chars": 3048,
"preview": "import { QueryInfo } from '@airbrake/browser';\nimport { Notifier } from '../notifier';\n\nconst SPAN_NAME = 'sql';\n\nexport"
},
{
"path": "packages/node/src/instrumentation/mysql2.ts",
"chars": 1980,
"preview": "import { QueryInfo } from '@airbrake/browser';\nimport { Notifier } from '../notifier';\n\nconst SPAN_NAME = 'sql';\n\nexport"
},
{
"path": "packages/node/src/instrumentation/pg.ts",
"chars": 1898,
"preview": "import { QueryInfo } from '@airbrake/browser';\nimport { Notifier } from '../notifier';\n\nconst SPAN_NAME = 'sql';\n\nexport"
},
{
"path": "packages/node/src/instrumentation/redis.ts",
"chars": 742,
"preview": "import { Notifier } from '../notifier';\n\nconst SPAN_NAME = 'redis';\n\nexport function patch(redis, airbrake: Notifier): v"
},
{
"path": "packages/node/src/notifier.ts",
"chars": 2879,
"preview": "import { BaseNotifier, INotice, IOptions } from '@airbrake/browser';\nimport { nodeFilter } from './filter/node';\nimport "
},
{
"path": "packages/node/src/scope.ts",
"chars": 816,
"preview": "import { Scope } from '@airbrake/browser';\nimport * as asyncHooks from 'async_hooks';\n\nexport { Scope };\n\nexport class S"
},
{
"path": "packages/node/src/version.ts",
"chars": 189,
"preview": "export const NOTIFIER_NAME = 'airbrake-js/node';\nexport const NOTIFIER_VERSION = '2.1.9';\nexport const NOTIFIER_URL =\n "
},
{
"path": "packages/node/tests/notifier.test.js",
"chars": 1398,
"preview": "import { Notifier } from '../src/notifier';\n\ndescribe('Notifier', () => {\n describe('configuration', () => {\n descri"
},
{
"path": "packages/node/tests/routes.test.js",
"chars": 1249,
"preview": "import { Notifier } from '../src/notifier';\n\ndescribe('Routes', () => {\n const opt = {\n projectId: 1,\n projectKey"
},
{
"path": "packages/node/tsconfig.cjs.json",
"chars": 110,
"preview": "{\n \"extends\": \"../../tsconfig.json\",\n \"compilerOptions\": {\n \"outDir\": \"dist\"\n },\n \"include\": [\"src\"]\n}\n"
},
{
"path": "packages/node/tsconfig.esm.json",
"chars": 113,
"preview": "{\n \"extends\": \"../../tsconfig.esm.json\",\n \"compilerOptions\": {\n \"outDir\": \"esm\"\n },\n \"include\": [\"src\"]\n}\n"
},
{
"path": "packages/node/tsconfig.json",
"chars": 117,
"preview": "{\n \"extends\": \"./tsconfig.cjs.json\",\n \"compilerOptions\": {\n \"rootDir\": \".\",\n },\n \"include\": [\"src\", \"test\"]\n}\n"
},
{
"path": "packages/node/tslint.json",
"chars": 39,
"preview": "{\n \"extends\": [\"../../tslint.json\"]\n}\n"
},
{
"path": "tsconfig.esm.json",
"chars": 83,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n \"module\": \"ES6\"\n }\n}\n"
},
{
"path": "tsconfig.json",
"chars": 367,
"preview": "{\n \"compilerOptions\": {\n \"declaration\": true,\n \"declarationMap\": true,\n \"esModuleInterop\": true,\n \"lib\": [\""
},
{
"path": "tslint.json",
"chars": 644,
"preview": "{\n \"extends\": [\n \"tslint:latest\",\n \"tslint-config-prettier\",\n \"tslint-plugin-prettier\"\n ],\n \"rules\": {\n \""
}
]
About this extraction
This page contains the full source code of the airbrake/airbrake-js GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 125 files (188.1 KB), approximately 53.7k tokens, and a symbol index with 260 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.