Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow async beforeNotify functions to modify the notice object #984

Merged
merged 17 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 118 additions & 71 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,89 +151,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)
subzero10 marked this conversation as resolved.
Show resolved Hide resolved
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
subzero10 marked this conversation as resolved.
Show resolved Hide resolved
}

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 @@ -418,4 +361,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)
subzero10 marked this conversation as resolved.
Show resolved Hide resolved
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[]) {
subzero10 marked this conversation as resolved.
Show resolved Hide resolved
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): boolean | void | Promise<void>
subzero10 marked this conversation as resolved.
Show resolved Hide resolved
}

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 (typeof stack === 'undefined') {
subzero10 marked this conversation as resolved.
Show resolved Hide resolved
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 } {
subzero10 marked this conversation as resolved.
Show resolved Hide resolved
subzero10 marked this conversation as resolved.
Show resolved Hide resolved
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 {
subzero10 marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
Loading