diff --git a/CHANGELOG.md b/CHANGELOG.md index 18868337..ece70f30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased][latest] ### Fixed - TypeScript: Add missing methods typedefs (#611) +- Internal: Fixed TypeScript types, added null checks, automated type declaration files ## [3.2.1] - 2021-05-17 ### Fixed diff --git a/README.md b/README.md index 9a10fe38..1b52a13d 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,20 @@ See https://github.com/honeybadger-io/honeybadger-js/blob/master/CHANGELOG.md ## Development 1. Run `npm install`. -2. To run the test suite by itself, use `npm test`. -3. To run the tests across all supported platforms, set up a [BrowserStack](https://www.browserstack.com/) +2. To run unit tests for both browser and server builds: `npm test`. Or separately: `npm run test:browser`, `npm run test:server`. +3. To run integration tests across all supported platforms, set up a [BrowserStack](https://www.browserstack.com/) account and use `BROWSERSTACK_USERNAME=your_username BROWSERSTACK_ACCESS_KEY=your-access-key npm run test:integration`. +4. To test the TypeScript type definitions: `npm run tsd`. + +### Bundling and types +This project is _isomorphic_, meaning it's a single library which contains both browser and server builds. It's written in TypeScript, and transpiled and bundled with Rollup. Our Rollup config generates three main files: +1. The server build, which transpiles `src/server.ts` and its dependencies into `dist/server/honeybadger.js`. +2. The browser build, which transpiles `src/browser.ts` and its dependencies into `dist/browser/honeybadger.js`. +3. The minified browser build, which transpiles `src/browser.ts` and its dependencies into `dist/browser/honeybadger.min.js` (+ source maps). + +In addition, the TypeScript type declaration for each build is generated into its `types/` directory (ie `dist/browser/types/browser.d.ts` and `dist/server/types/server.d.ts`). + +However, since the package is isomorphic, TypeScript users will likely be writing `import * as Honeybadger from '@honeybadger-io/js'` or `import Honeybadger = require('@honeybadger-io/js')` in their IDE. Our `package.json` has ` main` and `browser` fields that determine which build they get, but [there can only be a single type declaration file](https://github.com/Microsoft/TypeScript/issues/29128). So we use an extra file in the project root, `honeybadger.d.ts`, that combines the types from both builds. ## Releasing diff --git a/honeybadger.d.ts b/honeybadger.d.ts index e5fec628..704a17f4 100644 --- a/honeybadger.d.ts +++ b/honeybadger.d.ts @@ -1,109 +1,7 @@ // Type definitions for honeybadger.js // Project: https://github.com/honeybadger-io/honeybadger-js -import { NextFunction, Request, Response } from 'express' +import Server from './dist/server/types/server' +import Browser from './dist/browser/types/browser' -declare class Honeybadger { - public getVersion(): string - public factory(opts?: Partial): Honeybadger - public notify(notice: Error | string | Partial, name?: string | Partial, extra?: string | Partial): Honeybadger.Notice | false - public configure(opts: Partial): Honeybadger - public beforeNotify(func: Honeybadger.BeforeNotifyHandler): Honeybadger - public afterNotify(func: Honeybadger.AfterNotifyHandler): Honeybadger - public setContext(context: Record): Honeybadger - public resetContext(context?: Record): Honeybadger - public clear(): Honeybadger - public addBreadcrumb(message: string, opts?: Partial): Honeybadger - - // Server middleware - public requestHandler(req: Request, res: Response, next: NextFunction): void - public errorHandler(err: any, req: Request, _res: Response, next: NextFunction): unknown - public lambdaHandler(handler: any): (event: any, context: any, callback: any) => void -} - -declare namespace Honeybadger { - interface Logger { - log(...args: unknown[]): unknown - info(...args: unknown[]): unknown - debug(...args: unknown[]): unknown - warn(...args: unknown[]): unknown - error(...args: unknown[]): unknown - } - - interface Config { - apiKey: string | undefined - endpoint: string, - developmentEnvironments: string[] - environment: string | undefined - hostname: string | undefined - projectRoot: string | undefined - component: string | undefined - action: string | undefined - revision: string | undefined - disabled: boolean - debug: boolean - reportData: boolean - breadcrumbsEnabled: boolean | Partial<{ dom: boolean, network: boolean, navigation: boolean, console: boolean }> - maxBreadcrumbs: number - maxObjectDepth: number - logger: Logger - enableUncaught: boolean - afterUncaught: (err: Error) => void - enableUnhandledRejection: boolean - tags: string | string[] | unknown - filters: string[] - [x: string]: unknown - - // Browser - async: boolean - maxErrors: number - } - - interface BeforeNotifyHandler { - (notice: Notice): boolean | void - } - - interface AfterNotifyHandler { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (error: any, notice: Notice): boolean | void - } - - interface Notice { - id: string | undefined, - name: string, - message: string, - stack: string, - backtrace: BacktraceFrame[], - fingerprint?: string | undefined, - url?: string | undefined, - component?: string | undefined, - action?: string | undefined, - context: Record, - cgiData: Record, - params: Record, - session: Record, - headers: Record, - cookies: Record | string, - projectRoot?: string | undefined, - environment?: string | undefined, - revision?: string | undefined, - afterNotify?: AfterNotifyHandler - tags: string | string[] - } - - interface BacktraceFrame { - file: string, - method: string, - number: number, - column: number - } - - interface BreadcrumbRecord { - category: string, - message: string, - metadata: Record, - timestamp: string - } -} - -declare const singleton: Honeybadger -export = singleton +declare const Honeybadger: typeof Server & typeof Browser +export = Honeybadger diff --git a/package-lock.json b/package-lock.json index 7c8f7b53..2fc2a71e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "3.2.1", "license": "MIT", "dependencies": { + "@types/express": "^4.17.13", "stacktrace-parser": "^0.1.10" }, "devDependencies": { @@ -2193,12 +2194,50 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/body-parser": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", + "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "dev": true }, + "node_modules/@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", + "integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, "node_modules/@types/glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", @@ -2264,6 +2303,11 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, "node_modules/@types/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", @@ -2279,8 +2323,7 @@ "node_modules/@types/node": { "version": "14.0.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.19.tgz", - "integrity": "sha512-yf3BP/NIXF37BjrK5klu//asUWitOEoUP5xE1mhSUjazotwJ/eJDgEmMQNlOeWOVv72j24QQ+3bqXHE++CFGag==", - "dev": true + "integrity": "sha512-yf3BP/NIXF37BjrK5klu//asUWitOEoUP5xE1mhSUjazotwJ/eJDgEmMQNlOeWOVv72j24QQ+3bqXHE++CFGag==" }, "node_modules/@types/normalize-package-data": { "version": "2.4.0", @@ -2294,6 +2337,16 @@ "integrity": "sha512-hkc1DATxFLQo4VxPDpMH1gCkPpBbpOoJ/4nhuXw4n63/0R6bCpQECj4+K226UJ4JO/eJQz+1mC2I7JsWanAdQw==", "dev": true }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -2303,6 +2356,15 @@ "@types/node": "*" } }, + "node_modules/@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", @@ -15648,12 +15710,50 @@ "@babel/types": "^7.3.0" } }, + "@types/body-parser": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", + "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "requires": { + "@types/node": "*" + } + }, "@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "dev": true }, + "@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", + "integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, "@types/glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", @@ -15719,6 +15819,11 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, "@types/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", @@ -15734,8 +15839,7 @@ "@types/node": { "version": "14.0.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.19.tgz", - "integrity": "sha512-yf3BP/NIXF37BjrK5klu//asUWitOEoUP5xE1mhSUjazotwJ/eJDgEmMQNlOeWOVv72j24QQ+3bqXHE++CFGag==", - "dev": true + "integrity": "sha512-yf3BP/NIXF37BjrK5klu//asUWitOEoUP5xE1mhSUjazotwJ/eJDgEmMQNlOeWOVv72j24QQ+3bqXHE++CFGag==" }, "@types/normalize-package-data": { "version": "2.4.0", @@ -15749,6 +15853,16 @@ "integrity": "sha512-hkc1DATxFLQo4VxPDpMH1gCkPpBbpOoJ/4nhuXw4n63/0R6bCpQECj4+K226UJ4JO/eJQz+1mC2I7JsWanAdQw==", "dev": true }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -15758,6 +15872,15 @@ "@types/node": "*" } }, + "@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "@types/stack-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", diff --git a/package.json b/package.json index 362145f3..cb39079c 100644 --- a/package.json +++ b/package.json @@ -37,10 +37,11 @@ "test:integration:browserstack": "npm run test:integration", "test:integration:headless": "HEADLESS=1 npm run test:integration", "tsd": "npm run build && tsd", - "build": "rollup -c && cp honeybadger.d.ts dist/server && cp honeybadger.d.ts dist/browser", + "build": "rollup -c && node ./scripts/copy-typedefs.js", "release": "shipjs prepare" }, "dependencies": { + "@types/express": "^4.17.13", "stacktrace-parser": "^0.1.10" }, "devDependencies": { @@ -74,5 +75,10 @@ "dist", "honeybadger.d.ts" ], + "tsd": { + "compilerOptions": { + "strict": false + } + }, "types": "./honeybadger.d.ts" } diff --git a/rollup.config.js b/rollup.config.js index a4056dca..df72d906 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -7,6 +7,7 @@ import pkg from './package.json' // These plugins are used for all builds const sharedPlugins = [ replace({ + preventAssignment: false, exclude: 'node_modules/**', __VERSION__: pkg.version }), @@ -16,13 +17,25 @@ const sharedPlugins = [ // These plugins are used for UMD builds const umdPlugins = [ ...sharedPlugins, - typescript() + typescript({ + tsconfig: './tsconfig.json', + exclude: [ + "./src/server.ts", + "./src/server/**" + ] + }) ] // These plugins are used for Node builds const nodePlugins = [ ...sharedPlugins, - typescript() + typescript({ + tsconfig: './tsconfig.json', + exclude: [ + "./src/browser.ts", + "./src/browser/**" + ] + }) ] export default [ diff --git a/scripts/copy-typedefs.js b/scripts/copy-typedefs.js new file mode 100644 index 00000000..6f7e5e9f --- /dev/null +++ b/scripts/copy-typedefs.js @@ -0,0 +1,15 @@ +// Copy type declaration files for backwards compatibility with earlier versions of Honeybadger + +/* eslint-disable */ + +const fs = require('fs'); +fs.writeFileSync( + 'dist/browser/honeybadger.d.ts', + fs.readFileSync('dist/browser/types/browser.d.ts', 'utf8').replace(/'\.\//g, `'./types/`) +); +fs.writeFileSync( + 'dist/server/honeybadger.d.ts', + fs.readFileSync('dist/server/types/server.d.ts', 'utf8').replace(/'\.\//g, `'./types/`) +); + +console.info("Copied declaration files"); \ No newline at end of file diff --git a/src/browser.ts b/src/browser.ts index e0dce8fa..236f26ef 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,9 +1,9 @@ import Client from './core/client' -import { Config, Notice } from './core/types' +import { Config, Notice, BeforeNotifyHandler } from './core/types' import { merge, sanitize, filter, runAfterNotifyHandlers, objectIsExtensible, endpoint } from './core/util' import { encodeCookie, decodeCookie, preferCatch } from './browser/util' import { onError, ignoreNextOnError } from './browser/integrations/onerror' -import onUnhandlerRejection from './browser/integrations/onunhandledrejection' +import onUnhandledRejection from './browser/integrations/onunhandledrejection' import breadcrumbs from './browser/integrations/breadcrumbs' import timers from './browser/integrations/timers' import eventListeners from './browser/integrations/event_listeners' @@ -19,12 +19,15 @@ interface WrappedFunc { } class Honeybadger extends Client { + /** @internal */ private __errorsSent = 0 + /** @internal */ private __lastWrapErr = undefined config: BrowserConfig - protected __beforeNotifyHandlers = [ + /** @internal */ + protected __beforeNotifyHandlers: BeforeNotifyHandler[] = [ (notice: Notice) => { if (this.__exceedsMaxErrors()) { this.logger.debug('Dropping notice: max errors exceeded', notice) @@ -46,6 +49,10 @@ class Honeybadger extends Client { }) } + configure(opts: Partial = {}) { + return super.configure(opts) + } + resetMaxErrors(): number { return (this.__errorsSent = 0) } @@ -54,6 +61,7 @@ class Honeybadger extends Client { return new Honeybadger(opts) } + /** @internal */ protected __buildPayload(notice:Notice): Record> { const cgiData = { HTTP_USER_AGENT: undefined, @@ -77,12 +85,13 @@ class Honeybadger extends Client { } const payload = super.__buildPayload(notice) - payload.request.cgi_data = merge(cgiData, payload.request.cgi_data) + payload.request.cgi_data = merge(cgiData, payload.request.cgi_data as Record) return payload } - protected __send(notice:Notice): boolean { + /** @internal */ + protected __send(notice) { this.__incrementErrorsCount() const payload = this.__buildPayload(notice) @@ -117,9 +126,11 @@ class Honeybadger extends Client { return true } - // wrap always returns the same function so that callbacks can be removed via - // removeEventListener. - // eslint-disable-next-line @typescript-eslint/no-explicit-any + /** + * wrap always returns the same function so that callbacks can be removed via + * removeEventListener. + * @internal + */ __wrap(f:unknown, opts:Record = {}):WrappedFunc { const func = f as WrappedFunc if (!opts) { opts = {} } @@ -172,10 +183,12 @@ class Honeybadger extends Client { } } + /** @internal */ private __incrementErrorsCount(): number { return this.__errorsSent++ } + /** @internal */ private __exceedsMaxErrors(): boolean { return this.config.maxErrors && this.__errorsSent >= this.config.maxErrors } @@ -184,7 +197,7 @@ class Honeybadger extends Client { export default new Honeybadger({ __plugins: [ onError(), - onUnhandlerRejection(), + onUnhandledRejection(), timers(), eventListeners(), breadcrumbs() diff --git a/src/core/client.ts b/src/core/client.ts index a43e6919..366771e2 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -1,5 +1,7 @@ import { merge, mergeNotice, objectIsEmpty, makeNotice, makeBacktrace, runBeforeNotifyHandlers, newObject, logger, generateStackTrace, filter, filterUrl, formatCGIData } from './util' -import { Config, Logger, BeforeNotifyHandler, AfterNotifyHandler, Notice } from './types' +import { + Config, Logger, BreadcrumbRecord, BeforeNotifyHandler, AfterNotifyHandler, Notice, Noticeable +} from './types' const notifier = { name: 'honeybadger-js', @@ -20,12 +22,17 @@ const STRING_EMPTY = '' const NOT_BLANK = /\S/ export default class Client { + /** @internal */ private __pluginsExecuted = false - protected __context = {} - protected __breadcrumbs = [] - protected __beforeNotifyHandlers = [] - protected __afterNotifyHandlers = [] + /** @internal */ + protected __context: Record = {} + /** @internal */ + protected __breadcrumbs: BreadcrumbRecord[] = [] + /** @internal */ + protected __beforeNotifyHandlers: BeforeNotifyHandler[] = [] + /** @internal */ + protected __afterNotifyHandlers: AfterNotifyHandler[] = [] config: Config logger: Logger @@ -96,9 +103,9 @@ export default class Client { return this } - resetContext(context: Record): Client { + resetContext(context?: Record): Client { this.logger.warn('Deprecation warning: `Honeybadger.resetContext()` has been deprecated; please use `Honeybadger.clear()` instead.') - if (typeof context === 'object') { + if (typeof context === 'object' && context !== null) { this.__context = merge({}, context) } else { this.__context = {} @@ -112,7 +119,7 @@ export default class Client { return this } - notify(notice: Partial, name = undefined, extra = undefined): Record | false | unknown { + notify(notice: Noticeable, name: string | Partial = undefined, extra: Partial = undefined): Record | false | unknown { if (!this.config.apiKey) { this.logger.warn('Unable to send error report: no API key has been configured') return false @@ -136,9 +143,9 @@ export default class Client { } if (name) { - notice = mergeNotice(notice, name) + notice = mergeNotice(notice, name as Partial) } - if (typeof extra === 'object') { + if (typeof extra === 'object' && extra !== null) { notice = mergeNotice(notice, extra) } @@ -186,7 +193,7 @@ export default class Client { return this.__send(notice) } - addBreadcrumb(message: string, opts: Record): Client { + addBreadcrumb(message: string, opts?: Record): Client { if (!this.config.breadcrumbsEnabled) { return } opts = opts || {} @@ -196,9 +203,9 @@ export default class Client { const timestamp = new Date().toISOString() this.__breadcrumbs.push({ - category: category, + category: category as string, message: message, - metadata: metadata, + metadata: metadata as Record, timestamp: timestamp }) @@ -210,17 +217,20 @@ export default class Client { return this } - private __reportData(): boolean { + /** @internal */ + protected __reportData(): boolean { if (this.config.reportData !== null) { return this.config.reportData } return !(this.config.environment && this.config.developmentEnvironments.includes(this.config.environment)) } - protected __send(_notice: Partial): unknown { + /** @internal */ + protected __send(_notice: Partial): boolean { throw (new Error('Must implement send in subclass')) } + /** @internal */ protected __buildPayload(notice: Notice): Record> { const headers = filter(notice.headers, this.config.filters) || {} const cgiData = filter({ @@ -260,6 +270,7 @@ export default class Client { } } + /** @internal */ protected __constructTags(tags: unknown): Array { if (!tags) { return [] diff --git a/src/core/types.ts b/src/core/types.ts index edbcc76d..9baeaefd 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -9,19 +9,19 @@ export interface Logger { } export interface Config { - apiKey: string | undefined + apiKey?: string, endpoint: string, developmentEnvironments: string[], - environment: string | undefined - hostname: string | undefined - projectRoot: string | undefined - component: string | undefined - action: string | undefined - revision: string | undefined + environment?: string + hostname?: string + projectRoot?: string + component?: string + action?: string + revision?: string disabled: boolean debug: boolean reportData: boolean - breadcrumbsEnabled: boolean | Partial<{ dom: boolean, network: boolean, navigation: boolean, console: boolean }> + breadcrumbsEnabled: boolean | { dom?: boolean, network?: boolean, navigation?: boolean, console?: boolean} maxBreadcrumbs: number maxObjectDepth: number logger: Logger @@ -30,7 +30,7 @@ export interface Config { enableUnhandledRejection: boolean filters: string[] __plugins: Plugin[], - [x: string]: unknown, + tags: any, } export interface BeforeNotifyHandler { @@ -70,6 +70,8 @@ export interface Notice { afterNotify?: AfterNotifyHandler } +export type Noticeable = string | Error | Partial + export interface BacktraceFrame { file: string, method: string, diff --git a/src/core/util.ts b/src/core/util.ts index a76d34b9..e2a539a3 100644 --- a/src/core/util.ts +++ b/src/core/util.ts @@ -1,15 +1,17 @@ import * as stackTraceParser from 'stacktrace-parser' import Client from '../core/client' -import { Logger, Config, BacktraceFrame } from '../core/types' +import { + Logger, Config, BacktraceFrame, Notice, Noticeable, BeforeNotifyHandler, AfterNotifyHandler +} from './types' -export function merge(obj1: any, obj2: any): any { - const result = {} +export function merge, T2 extends Record>(obj1: T1, obj2: T2): T1 &T2 { + const result = {} as Record for (const k in obj1) { result[k] = obj1[k] } for (const k in obj2) { result[k] = obj2[k] } - return result + return result as T1 & T2 } -export function mergeNotice(notice1: any, notice2: any): any { +export function mergeNotice(notice1: Partial, notice2: Partial): Partial { const result = merge(notice1, notice2) if (notice1.context && notice2.context) { result.context = merge(notice1.context, notice2.context) @@ -17,7 +19,7 @@ export function mergeNotice(notice1: any, notice2: any): any { return result } -export function objectIsEmpty(obj) { +export function objectIsEmpty(obj): boolean { for (const k in obj) { if (Object.prototype.hasOwnProperty.call(obj, k)) { return false @@ -26,7 +28,7 @@ export function objectIsEmpty(obj) { return true } -export function objectIsExtensible(obj) { +export function objectIsExtensible(obj): boolean { if (typeof Object.isExtensible !== 'function') { return true } return Object.isExtensible(obj) } @@ -49,7 +51,7 @@ export function makeBacktrace(stack: string, shift = 0): BacktraceFrame[] { } } -export function runBeforeNotifyHandlers(notice, handlers) { +export function runBeforeNotifyHandlers(notice, handlers: BeforeNotifyHandler[]): boolean { for (let i = 0, len = handlers.length; i < len; i++) { const handler = handlers[i] if (handler(notice) === false) { @@ -59,7 +61,7 @@ export function runBeforeNotifyHandlers(notice, handlers) { return true } -export function runAfterNotifyHandlers(notice, handlers, error = undefined) { +export function runAfterNotifyHandlers(notice, handlers: AfterNotifyHandler[], error = undefined): boolean { for (let i = 0, len = handlers.length; i < len; i++) { handlers[i](error, notice) } @@ -67,9 +69,9 @@ export function runAfterNotifyHandlers(notice, handlers, error = undefined) { } // Returns a new object with properties from other object. -export function newObject(obj) { - if (typeof (obj) !== 'object') { return {} } - const result = {} +export function newObject(obj: T): T|Record { + if (typeof (obj) !== 'object' || obj === null) { return {} } + const result = {} as T for (const k in obj) { result[k] = obj[k] } return result } @@ -157,16 +159,15 @@ export function logger(client: Client): Logger { /** * Converts any object into a notice object (which at minimum has the same * properties as Error, but supports additional Honeybadger properties.) - * @param {!Object} notice */ -export function makeNotice(thing) { +export function makeNotice(thing: Noticeable): Partial { let notice if (!thing) { notice = {} } else if (Object.prototype.toString.call(thing) === '[object Error]') { - const e = thing - notice = merge(thing, { name: e.name, message: e.message, stack: e.stack }) + const e = thing as Error + notice = merge(thing as Record, { name: e.name, message: e.message, stack: e.stack }) } else if (typeof thing === 'object') { notice = newObject(thing) } else { @@ -280,7 +281,7 @@ export function filter(obj: Record, filters: string[]): Record< return filter(obj) } -function filterMatch(key: string, filters: string[]) { +function filterMatch(key: string, filters: string[]): boolean { for (let i = 0; i < filters.length; i++) { if (key.toLowerCase().indexOf(filters[i].toLowerCase()) !== -1) { return true @@ -289,7 +290,7 @@ function filterMatch(key: string, filters: string[]) { return false } -function is(type: string, obj: any) { +function is(type: string, obj: unknown): boolean { const klass = Object.prototype.toString.call(obj).slice(8, -1) return obj !== undefined && obj !== null && klass === type } @@ -298,7 +299,7 @@ export function filterUrl(url:string, filters: string[]): string { if (!filters) { return url } if (typeof url !== 'string') { return url } - const [_, query] = url.split(/\?/, 2) + const query = url.split(/\?/, 2)[1] if (!query) { return url } let result = url diff --git a/src/server.ts b/src/server.ts index 73b8e756..81273b21 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,7 +4,7 @@ import { URL } from 'url' import os from 'os' import Client from './core/client' -import { Config, Notice } from './core/types' +import { Config, Notice, BeforeNotifyHandler } from './core/types' import { merge, sanitize, runAfterNotifyHandlers, endpoint } from './core/util' import { fatallyLogAndExit, getStats } from './server/util' import uncaughtException from './server/integrations/uncaught_exception' @@ -12,7 +12,8 @@ import unhandledRejection from './server/integrations/unhandled_rejection' import { errorHandler, requestHandler, lambdaHandler } from './server/middleware' class Honeybadger extends Client { - protected __beforeNotifyHandlers = [ + /** @internal */ + protected __beforeNotifyHandlers: BeforeNotifyHandler[] = [ (notice: Notice) => { notice.backtrace.forEach((line) => { if (line.file) { @@ -24,6 +25,10 @@ class Honeybadger extends Client { } ] + public errorHandler: typeof errorHandler; + public requestHandler: typeof requestHandler; + public lambdaHandler: typeof lambdaHandler; + constructor(opts: Partial = {}) { super({ afterUncaught: fatallyLogAndExit, @@ -31,17 +36,17 @@ class Honeybadger extends Client { hostname: os.hostname(), ...opts, }) + this.errorHandler = errorHandler.bind(this) + this.requestHandler = requestHandler.bind(this) + this.lambdaHandler = lambdaHandler.bind(this) } - errorHandler = errorHandler.bind(this) - requestHandler = requestHandler.bind(this) - lambdaHandler = lambdaHandler.bind(this) - factory(opts?: Partial): Honeybadger { return new Honeybadger(opts) } - protected __send(notice: Notice): boolean { + /** @internal */ + protected __send(notice) { const { protocol } = new URL(this.config.endpoint) const transport = (protocol === "http:" ? http : https) diff --git a/src/server/integrations/unhandled_rejection.ts b/src/server/integrations/unhandled_rejection.ts index 3fb995e2..0490878c 100644 --- a/src/server/integrations/unhandled_rejection.ts +++ b/src/server/integrations/unhandled_rejection.ts @@ -1,5 +1,4 @@ import { Plugin } from '../../core/types' -import { fatallyLogAndExit } from '../../server/util' import Client from '../../server' export default function (): Plugin { diff --git a/src/server/middleware.ts b/src/server/middleware.ts index 9bba126d..4c93de88 100644 --- a/src/server/middleware.ts +++ b/src/server/middleware.ts @@ -1,11 +1,14 @@ import url from 'url' import domain from 'domain' +import { NextFunction, Request, Response } from 'express' -function fullUrl(req) { +function fullUrl(req: Request): string { const connection = req.connection const address = connection && connection.address() + // @ts-ignore The old @types/node incorrectly defines `address` as string|Address const port = address ? address.port : undefined + // @ts-ignore return url.format({ protocol: req.protocol, hostname: req.hostname, @@ -15,17 +18,18 @@ function fullUrl(req) { }) } -function requestHandler(req, res, next) { +function requestHandler(req: Request, res: Response, next: NextFunction): void { this.clear() const dom = domain.create() dom.on('error', next) dom.run(next) } -function errorHandler(err, req, _res, next) { +function errorHandler(err: any, req: Request, _res: Response, next: NextFunction): unknown { this.notify(err, { url: fullUrl(req), params: req.body, // http://expressjs.com/en/api.html#req.body + // @ts-ignore session: req.session, // https://github.com/expressjs/session#reqsession headers: req.headers, // https://nodejs.org/api/http.html#http_message_headers cgiData: { @@ -35,7 +39,9 @@ function errorHandler(err, req, _res, next) { return next(err) } -function lambdaHandler(handler) { +type LambdaHandler = (event: unknown, context: unknown, callback: unknown) => void|Promise + +function lambdaHandler(handler: LambdaHandler): LambdaHandler { return function lambdaHandler(event, context, callback) { // eslint-disable-next-line prefer-rest-params const args = arguments diff --git a/src/server/util.ts b/src/server/util.ts index 0be66dcf..34133c5a 100644 --- a/src/server/util.ts +++ b/src/server/util.ts @@ -1,7 +1,7 @@ import os from 'os' import fs from 'fs' -export function fatallyLogAndExit(err: Error): void { +export function fatallyLogAndExit(err: Error): never { console.error('[Honeybadger] Exiting process due to uncaught exception') console.error(err.stack || err) process.exit(1) diff --git a/test-d/browser.test-d.tsx b/test-d/browser.test-d.tsx index 54e644ec..fe08396a 100644 --- a/test-d/browser.test-d.tsx +++ b/test-d/browser.test-d.tsx @@ -12,8 +12,6 @@ Honeybadger.configure({ revision: 'git SHA/project version', component: 'example_comonent', action: 'example_action', - onerror: true, - onunhandledrejection: true, async: true, maxErrors: 20, breadcrumbsEnabled: true diff --git a/test-d/server.test-d.tsx b/test-d/server.test-d.tsx index 944365fa..041ca5d1 100644 --- a/test-d/server.test-d.tsx +++ b/test-d/server.test-d.tsx @@ -1,5 +1,22 @@ import Honeybadger from '../dist/server/honeybadger' +Honeybadger.configure({ + debug: false, + disabled: true, + endpoint: 'https://api.honeybadger.io', + projectRoot: 'webpack:///./', + apiKey: 'project api key', + environment: 'production', + hostname: 'badger01', + revision: 'git SHA/project version', + component: 'example_comonent', + action: 'example_action', + breadcrumbsEnabled: true +}) + +Honeybadger.resetContext({ + user_id: 123 +}) Honeybadger.notify('test') Honeybadger.notify(new Error('test')) Honeybadger.notify({ message: 'test' }) diff --git a/test/unit/core/util.test.ts b/test/unit/core/util.test.ts index bc7d3361..ed93ac66 100644 --- a/test/unit/core/util.test.ts +++ b/test/unit/core/util.test.ts @@ -71,8 +71,8 @@ describe('utils', function () { }) describe('mergeNotice', function () { - it('combines two objects', function () { - expect(mergeNotice({ foo: 'foo' }, { bar: 'bar' })).toEqual({ foo: 'foo', bar: 'bar' }) + it('combines two notice objects', function () { + expect(mergeNotice({ name: 'foo' }, { message: 'bar' })).toEqual({ name: 'foo', message: 'bar' }) }) it('combines context properties', function () { diff --git a/test/unit/helpers.ts b/test/unit/helpers.ts index f3bf6027..3b5d4571 100644 --- a/test/unit/helpers.ts +++ b/test/unit/helpers.ts @@ -12,7 +12,8 @@ export function nullLogger (): Logger { } export class TestClient extends BaseClient { - protected __send (notice: Notice): unknown { + // @ts-ignore + protected __send(notice) { return this.__buildPayload(notice) } } diff --git a/tsconfig.json b/tsconfig.json index f4a08a88..2d9802c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,10 @@ "allowJs": false, "target": "es5", "esModuleInterop": true, - "noImplicitAny": false + "noImplicitAny": false, + "declaration": true, + "declarationDir": "types", + "stripInternal": true }, "include": [ "./src/**/*"