Skip to content

Commit

Permalink
Merge pull request #665 from bugsnag/v7-callbacks
Browse files Browse the repository at this point in the history
V7: Callback refactor
  • Loading branch information
bengourley authored Dec 9, 2019
2 parents 32071c9 + 852e01d commit 057eae2
Show file tree
Hide file tree
Showing 31 changed files with 321 additions and 65 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- Rename `metaData` -> `metadata` and add consistent `add/get/clearMetadata()` methods to `Client`/`Event` for manipulating metadata explicitly, rather than mutating a property [#658](https://github.com/bugsnag/bugsnag-js/pull/658)
- Remove non-public methods from `Client` interface: `logger()`, `delivery()` and `sessionDelegate()` [#659](https://github.com/bugsnag/bugsnag-js/pull/659)
- Update `leaveBreadcrumb()` type signature to return `void`. [#661](https://github.com/bugsnag/bugsnag-js/pull/661)
- Add `onBreadcrumb` and `onSession` callbacks. [#665](https://github.com/bugsnag/bugsnag-js/pull/665)

## 6.4.3 (2019-10-21)

Expand Down
18 changes: 14 additions & 4 deletions packages/browser/types/test/fixtures/all-options.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import bugsnag from "../../.."
import bugsnag, { Bugsnag } from "../../.."
bugsnag({
apiKey: "abc",
appVersion: "1.2.3",
appType: "worker",
autoDetectErrors: true,
autoDetectUnhandledRejections: true,
onError: [],
onError: [
event => true
],
onBreadcrumb: (b: Bugsnag.Breadcrumb) => {
console.log(b.message)
return false
},
onSession: (s: Bugsnag.Session) => {
console.log(s.id)
return true
},
endpoints: {"notify":"https://notify.bugsnag.com","sessions":"https://sessions.bugsnag.com"},
autoTrackSessions: true,
enabledReleaseStages: [],
enabledReleaseStages: ['zzz'],
releaseStage: "production",
maxBreadcrumbs: 20,
enabledBreadcrumbTypes: ['manual','log','request'],
user: null,
metaData: {},
metadata: {},
logger: undefined,
filters: ["foo",/bar/],
collectUserIp: true,
Expand Down
80 changes: 71 additions & 9 deletions packages/core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ const config = require('./config')
const BugsnagEvent = require('./event')
const BugsnagBreadcrumb = require('./breadcrumb')
const BugsnagSession = require('./session')
const { map, includes } = require('./lib/es-utils')
const { map, includes, filter } = require('./lib/es-utils')
const inferReleaseStage = require('./lib/infer-release-stage')
const isError = require('./lib/iserror')
const runCallbacks = require('./lib/callback-runner')
const metadataDelegate = require('./lib/metadata-delegate')
const runSyncCallbacks = require('./lib/sync-callback-runner')

const noop = () => {}

const LOG_USAGE_ERR_PREFIX = 'Usage error.'
const REPORT_USAGE_ERR_PREFIX = 'Bugsnag usage error.'
Expand All @@ -22,8 +25,8 @@ class BugsnagClient {
this._schema = schema

// // i/o
this._delivery = { sendSession: () => {}, sendEvent: () => {} }
this._logger = { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} }
this._delivery = { sendSession: noop, sendEvent: noop }
this._logger = { debug: noop, info: noop, warn: noop, error: noop }

// plugins
this._plugins = {}
Expand All @@ -41,6 +44,20 @@ class BugsnagClient {
this.request = undefined
this._user = {}

// callbacks:
// e: onError
// s: onSession
// sp: onSessionPayload
// b: onBreadcrumb
// (note these names are minified by hand because object
// properties are not safe to minify automatically)
this._cbs = {
e: [],
s: [],
sp: [],
b: []
}

// expose internal constructors
this.BugsnagClient = BugsnagClient
this.BugsnagEvent = BugsnagEvent
Expand Down Expand Up @@ -75,13 +92,17 @@ class BugsnagClient {
if (!validity.valid === true) throw new Error(generateConfigErrorMessage(validity.errors))

// update and elevate some special options if they were passed in at this point
if (typeof conf.onError === 'function') conf.onError = [conf.onError]
if (conf.appVersion) this.app.version = conf.appVersion
if (conf.appType) this.app.type = conf.appType
if (conf.metadata) this._metadata = conf.metadata
if (conf.user) this._user = conf.user
if (conf.logger) this._logger = conf.logger

// add callbacks
if (conf.onError && conf.onError.length) this._cbs.e = this._cbs.e.concat(conf.onError)
if (conf.onBreadcrumb && conf.onBreadcrumb.length) this._cbs.b = this._cbs.b.concat(conf.onBreadcrumb)
if (conf.onSession && conf.onSession.length) this._cbs.s = this._cbs.s.concat(conf.onSession)

// merge with existing config
this._config = { ...this._config, ...conf }
}
Expand Down Expand Up @@ -114,11 +135,44 @@ class BugsnagClient {
}

startSession () {
if (!this._sessionDelegate) {
this._logger.warn('No session implementation is installed')
const session = new BugsnagSession()

// run onSession callbacks
const ignore = runSyncCallbacks(this._cbs.s.slice(0), session, 'onSession', this._logger)

if (ignore) {
this._logger.debug('Session not started due to onSession callback')
return this
}
return this._sessionDelegate.startSession(this)
return this._sessionDelegate.startSession(this, session)
}

addOnError (fn, front = false) {
this._cbs.e[front ? 'unshift' : 'push'](fn)
}

removeOnError (fn) {
this._cbs.e = filter(this._cbs.e, f => f !== fn)
}

_addOnSessionPayload (fn) {
this._cbs.sp.push(fn)
}

addOnSession (fn) {
this._cbs.s.push(fn)
}

removeOnSession (fn) {
this._cbs.s = filter(this._cbs.s, f => f !== fn)
}

addOnBreadcrumb (fn) {
this._cbs.b.push(fn)
}

removeOnBreadcrumb (fn) {
this._cbs.b = filter(this._cbs.b, f => f !== fn)
}

leaveBreadcrumb (message, metadata, type) {
Expand All @@ -135,14 +189,22 @@ class BugsnagClient {

const crumb = new BugsnagBreadcrumb(message, metadata, type)

// run onBreadcrumb callbacks
const ignore = runSyncCallbacks(this._cbs.b.slice(0), crumb, 'onBreadcrumb', this._logger)

if (ignore) {
this._logger.debug('Breadcrumb not attached due to onBreadcrumb callback')
return
}

// push the valid crumb onto the queue and maintain the length
this.breadcrumbs.push(crumb)
if (this.breadcrumbs.length > this._config.maxBreadcrumbs) {
this.breadcrumbs = this.breadcrumbs.slice(this.breadcrumbs.length - this._config.maxBreadcrumbs)
}
}

notify (error, onError, cb = () => {}) {
notify (error, onError, cb = noop) {
// releaseStage can be set via config.releaseStage or client.app.releaseStage
const releaseStage = inferReleaseStage(this)

Expand Down Expand Up @@ -179,7 +241,7 @@ class BugsnagClient {
this._logger.error(err)
}

const callbacks = [].concat(onError).concat(this._config.onError)
const callbacks = [].concat(this._cbs.e).concat(onError)
runCallbacks(callbacks, event, onCallbackError, (err, shouldSend) => {
if (err) onCallbackError(err)

Expand Down
14 changes: 12 additions & 2 deletions packages/core/config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { filter, reduce, keys, isArray, includes } = require('./lib/es-utils')
const { intRange, stringWithLength } = require('./lib/validators')
const { intRange, stringWithLength, listOfFunctions } = require('./lib/validators')

const BREADCRUMB_TYPES = ['navigation', 'request', 'process', 'log', 'user', 'state', 'error', 'manual']

Expand Down Expand Up @@ -32,7 +32,17 @@ module.exports.schema = {
onError: {
defaultValue: () => [],
message: 'should be a function or array of functions',
validate: value => typeof value === 'function' || (isArray(value) && filter(value, f => typeof f === 'function').length === value.length)
validate: listOfFunctions
},
onSession: {
defaultValue: () => [],
message: 'should be a function or array of functions',
validate: listOfFunctions
},
onBreadcrumb: {
defaultValue: () => [],
message: 'should be a function or array of functions',
validate: listOfFunctions
},
endpoints: {
defaultValue: () => ({
Expand Down
7 changes: 7 additions & 0 deletions packages/core/lib/clone-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ module.exports = (client) => {
clone.request = { ...client.request }
clone._user = { ...client._user }

clone._cbs = {
e: client._cbs.e.slice(),
s: client._cbs.s.slice(),
sp: client._cbs.sp.slice(),
b: client._cbs.b.slice()
}

clone._logger = client._logger
clone._delivery = client._delivery

Expand Down
14 changes: 14 additions & 0 deletions packages/core/lib/sync-callback-runner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = (callbacks, callbackArg, callbackType, logger) => {
let ignore = false
const cbs = callbacks.slice(0)
while (!ignore) {
if (!cbs.length) break
try {
ignore = cbs.pop()(callbackArg) === false
} catch (e) {
logger.error(`Error occurred in ${callbackType} callback, continuing anyway…`)
logger.error(e)
}
}
return ignore
}
4 changes: 4 additions & 0 deletions packages/core/lib/validators.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
const { filter, isArray } = require('./es-utils')

exports.intRange = (min = 1, max = Infinity) => value =>
typeof value === 'number' &&
parseInt('' + value, 10) === value &&
value >= min && value <= max

exports.stringWithLength = value => typeof value === 'string' && !!value.length

exports.listOfFunctions = value => typeof value === 'function' || (isArray(value) && filter(value, f => typeof f === 'function').length === value.length)
Loading

0 comments on commit 057eae2

Please sign in to comment.