Skip to content

Commit

Permalink
feat: allow async beforeNotify functions to modify the notice object (#…
Browse files Browse the repository at this point in the history
…984)

Co-authored-by: Pangratios Cosma <[email protected]>
  • Loading branch information
KonnorRogers and subzero10 authored Jul 27, 2023
1 parent ba7fa2c commit bcb2b92
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 104 deletions.
189 changes: 118 additions & 71 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,89 +164,32 @@ export abstract class Client {
}

notify(noticeable: Noticeable, name: string | Partial<Notice> = undefined, extra: Partial<Notice> = undefined): boolean {
let preConditionError: Error = null
const notice = this.makeNotice(noticeable, name, extra)
if (!notice) {
this.logger.debug('failed to build error report')
preConditionError = new Error('failed to build error report')
}

if (!preConditionError && this.config.reportData === false) {
this.logger.debug('skipping error report: honeybadger.js is disabled', notice)
preConditionError = new Error('honeybadger.js is disabled')
}

if (!preConditionError && this.__developmentMode()) {
this.logger.log('honeybadger.js is in development mode; the following error report will be sent in production.', notice)
preConditionError = new Error('honeybadger.js is in development mode')
}

if (!preConditionError && !this.config.apiKey) {
this.logger.warn('could not send error report: no API key has been configured', notice)
preConditionError = new Error('missing API key')
}
// we need to have the source file data before the beforeNotifyHandlers,
// in case they modify them
const sourceCodeData = notice && notice.backtrace ? notice.backtrace.map(trace => shallowClone(trace) as BacktraceFrame) : null
const beforeNotifyResult = runBeforeNotifyHandlers(notice, this.__beforeNotifyHandlers)
if (!preConditionError && !beforeNotifyResult) {
this.logger.debug('skipping error report: beforeNotify handlers returned false', notice)
preConditionError = new Error('beforeNotify handlers returned false')
}
const preConditionsResult = this.__runPreconditions(notice)
if (preConditionsResult instanceof Error) {
runAfterNotifyHandlers(notice, this.__afterNotifyHandlers, preConditionsResult)

if (preConditionError) {
runAfterNotifyHandlers(notice, this.__afterNotifyHandlers, preConditionError)
return false
}

this.addBreadcrumb('Honeybadger Notice', {
category: 'notice',
metadata: {
message: notice.message,
name: notice.name,
stack: notice.stack
}
})
if (preConditionsResult instanceof Promise) {
preConditionsResult.then((result) => {
if (result instanceof Error) {
runAfterNotifyHandlers(notice, this.__afterNotifyHandlers, result)

const breadcrumbs = this.__store.getContents('breadcrumbs')
notice.__breadcrumbs = this.config.breadcrumbsEnabled ? breadcrumbs : []
return false
}
return this.__send(notice, sourceCodeData)
})

getSourceForBacktrace(sourceCodeData, this.__getSourceFileHandler)
.then(sourcePerTrace => {
sourcePerTrace.forEach((source, index) => {
notice.backtrace[index].source = source
})
return true
}

const payload = this.__buildPayload(notice)
this.__transport
.send({
headers: {
'X-API-Key': this.config.apiKey,
'Content-Type': 'application/json',
'Accept': 'text/json, application/json'
},
method: 'POST',
endpoint: endpoint(this.config.endpoint, '/v1/notices/js'),
maxObjectDepth: this.config.maxObjectDepth,
logger: this.logger,
}, payload)
.then(res => {
if (res.statusCode !== 201) {
runAfterNotifyHandlers(notice, this.__afterNotifyHandlers, new Error(`Bad HTTP response: ${res.statusCode}`))
this.logger.warn(`Error report failed: unknown response from server. code=${res.statusCode}`)
return
}
const uuid = JSON.parse(res.body).id
runAfterNotifyHandlers(merge(notice, {
id: uuid
}), this.__afterNotifyHandlers)
this.logger.info(`Error report sent ⚡ https://app.honeybadger.io/notice/${uuid}`)
})
.catch(err => {
this.logger.error('Error report failed: an unknown error occurred.', `message=${err.message}`)
runAfterNotifyHandlers(notice, this.__afterNotifyHandlers, err)
})
})
this.__send(notice, sourceCodeData).catch((_err) => { /* error is already caught and logged */ })

return true
}
Expand Down Expand Up @@ -431,4 +374,108 @@ export abstract class Client {

return tags.toString().split(TAG_SEPARATOR).filter((tag) => NOT_BLANK.test(tag))
}

private __runPreconditions(notice: Notice): Error | null | Promise<Error | null> {
let preConditionError: Error | null = null
if (!notice) {
this.logger.debug('failed to build error report')
preConditionError = new Error('failed to build error report')
}

if (this.config.reportData === false) {
this.logger.debug('skipping error report: honeybadger.js is disabled', notice)
preConditionError = new Error('honeybadger.js is disabled')
}

if (this.__developmentMode()) {
this.logger.log('honeybadger.js is in development mode; the following error report will be sent in production.', notice)
preConditionError = new Error('honeybadger.js is in development mode')
}

if (!this.config.apiKey) {
this.logger.warn('could not send error report: no API key has been configured', notice)
preConditionError = new Error('missing API key')
}

const beforeNotifyResult = runBeforeNotifyHandlers(notice, this.__beforeNotifyHandlers)
if (!preConditionError && !beforeNotifyResult.result) {
this.logger.debug('skipping error report: one or more beforeNotify handlers returned false', notice)
preConditionError = new Error('beforeNotify handlers returned false')
}

if (beforeNotifyResult.results.length && beforeNotifyResult.results.some(result => result instanceof Promise)) {
return Promise.allSettled(beforeNotifyResult.results)
.then((results) => {
if (!preConditionError && (results.some(result => result.status === 'rejected' || result.value === false))) {
this.logger.debug('skipping error report: one or more beforeNotify handlers returned false', notice)
preConditionError = new Error('beforeNotify handlers (async) returned false')
}

if (preConditionError) {
return preConditionError
}
})
}

return preConditionError
}

private __send(notice: Notice, originalBacktrace: BacktraceFrame[]) {
if (this.config.breadcrumbsEnabled) {
this.addBreadcrumb('Honeybadger Notice', {
category: 'notice',
metadata: {
message: notice.message,
name: notice.name,
stack: notice.stack
}
})
notice.__breadcrumbs = this.__store.getContents('breadcrumbs')
}
else {
notice.__breadcrumbs = []
}

return getSourceForBacktrace(originalBacktrace, this.__getSourceFileHandler)
.then(async (sourcePerTrace) => {
sourcePerTrace.forEach((source, index) => {
notice.backtrace[index].source = source
})

const payload = this.__buildPayload(notice)
return this.__transport
.send({
headers: {
'X-API-Key': this.config.apiKey,
'Content-Type': 'application/json',
'Accept': 'text/json, application/json'
},
method: 'POST',
endpoint: endpoint(this.config.endpoint, '/v1/notices/js'),
maxObjectDepth: this.config.maxObjectDepth,
logger: this.logger,
}, payload)
})
.then(res => {
if (res.statusCode !== 201) {
runAfterNotifyHandlers(notice, this.__afterNotifyHandlers, new Error(`Bad HTTP response: ${res.statusCode}`))
this.logger.warn(`Error report failed: unknown response from server. code=${res.statusCode}`)

return false
}
const uuid = JSON.parse(res.body).id
runAfterNotifyHandlers(merge(notice, {
id: uuid
}), this.__afterNotifyHandlers)
this.logger.info(`Error report sent ⚡ https://app.honeybadger.io/notice/${uuid}`)

return true
})
.catch(err => {
this.logger.error('Error report failed: an unknown error occurred.', `message=${err.message}`)
runAfterNotifyHandlers(notice, this.__afterNotifyHandlers, err)

return false
})
}
}
4 changes: 2 additions & 2 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ export interface ServerlessConfig extends Config {
}

export interface BeforeNotifyHandler {
(notice?: Notice): boolean | void
(notice?: Notice): boolean | void | Promise<boolean | void>
}

export interface AfterNotifyHandler {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(error: any, notice?: Notice): boolean | void
(error: any, notice?: Notice): void | Promise<void>
}

export interface Plugin {
Expand Down
18 changes: 15 additions & 3 deletions packages/core/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export function objectIsExtensible(obj): boolean {
}

export function makeBacktrace(stack: string, filterHbSourceCode = false, logger: Logger = console): BacktraceFrame[] {
if (!stack) {
return []
}

try {
const backtrace = stackTraceParser
.parse(stack)
Expand Down Expand Up @@ -154,15 +158,23 @@ export async function getSourceForBacktrace(backtrace: BacktraceFrame[],
return result
}

export function runBeforeNotifyHandlers(notice: Notice | null, handlers: BeforeNotifyHandler[]): boolean {
export function runBeforeNotifyHandlers(notice: Notice | null, handlers: BeforeNotifyHandler[]): { results: ReturnType<BeforeNotifyHandler>[], result: boolean } {
const results: ReturnType<BeforeNotifyHandler>[] = []
let result = true
for (let i = 0, len = handlers.length; i < len; i++) {
const handler = handlers[i]
if (handler(notice) === false) {
const handlerResult = handler(notice)
if (handlerResult === false) {
result = false
}

results.push(handlerResult)
}

return {
results,
result
}
return result
}

export function runAfterNotifyHandlers(notice: Notice | null, handlers: AfterNotifyHandler[], error?: Error): boolean {
Expand Down
Loading

0 comments on commit bcb2b92

Please sign in to comment.