Skip to content

Commit

Permalink
#327 notifyAsync (#684)
Browse files Browse the repository at this point in the history
* notifyAsync + tests

* simplify implementation

* changelog

* add test

* revert to original implementation + test
  • Loading branch information
subzero10 authored Jan 20, 2022
1 parent b6aa667 commit b26566a
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased][latest]
### Added
- Nodejs: Include source snippet in backtraces when available (#624)
- `notifyAsync`: Async version of `notify` that returns a promise (#327)

### Changed
- Call afterNotify handlers with error if notify preconditions fail (#654)
Expand Down
45 changes: 45 additions & 0 deletions src/core/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,51 @@ export default class Client {
return true
}

/**
* An async version of {@link notify} that resolves only after the notice has been reported to Honeybadger.
* Implemented using the {@link afterNotify} hook.
* Rejects if for any reason the report failed to be reported.
* Useful in serverless environments (AWS Lambda).
*/
notifyAsync(noticeable: Noticeable, name: string | Partial<Notice> = undefined, extra: Partial<Notice> = undefined): Promise<void> {
return new Promise((resolve, reject) => {
const applyAfterNotify = (partialNotice: Partial<Notice>) => {
const originalAfterNotify = partialNotice.afterNotify
partialNotice.afterNotify = (err?: Error) => {
originalAfterNotify?.call(this, err)
if (err) {
return reject(err)
}
resolve()
}
}

// We have to respect any afterNotify hooks that come from the arguments
let objectToOverride: Partial<Notice>
if ((noticeable as Partial<Notice>).afterNotify) {
objectToOverride = noticeable as Partial<Notice>
}
else if (name && (name as Partial<Notice>).afterNotify) {
objectToOverride = name as Partial<Notice>
}
else if (extra && extra.afterNotify) {
objectToOverride = extra
}
else if (name && typeof name === 'object') {
objectToOverride = name
}
else if (extra) {
objectToOverride = extra
}
else {
objectToOverride = name = {}
}

applyAfterNotify(objectToOverride)
this.notify(noticeable, name, extra)
})
}

protected makeNotice(noticeable: Noticeable, name: string | Partial<Notice> = undefined, extra: Partial<Notice> = undefined): Notice | null {
let notice = makeNotice(noticeable)

Expand Down
97 changes: 97 additions & 0 deletions test/unit/core/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,103 @@ describe('client', function () {
})
})

describe('notifyAsync', function () {
beforeEach(() => {
client.configure({
apiKey: 'testing'
})
})

it('resolves when configured', async () => {
await client.notifyAsync(new Error('test'))
})

it('calls afterNotify from client.afterNotify', async () => {
let called = false
client.afterNotify((_err) => {
called = true
})

await client.notifyAsync('test test')

expect(called).toBeTruthy()
})

it('calls afterNotify in noticeable', async () => {
let called = false
const afterNotify = () => {
called = true
}

await client.notifyAsync({
message: 'test',
afterNotify
})

expect(called).toBeTruthy()
})

it('calls afterNotify in name', async () => {
let called = false
const afterNotify = () => {
called = true
}

await client.notifyAsync(new Error('test'), { afterNotify })

expect(called).toBeTruthy()
})

it('calls afterNotify in extra', async () => {
let called = false
const afterNotify = () => {
called = true
}

await client.notifyAsync(new Error('test'), 'an error', { afterNotify })

expect(called).toBeTruthy()
})

it('calls afterNotify and then resolves promise', async () => {
// the afterNotify hook that resolves the promise is called first
// however, the loop continues to call all handlers before it gives back
// control to the event loop
// which means: all afterNotify hooks will be called and then the promise will resolve
const called: boolean[] = [];
function register(i: number) {
called[i] = false
client.afterNotify(() => {
called[i] = true
})
}

for (let i =0; i < 100; i++) {
register(i)
}

await client.notifyAsync(new Error('test'))

expect(called.every(val => val === true)).toBeTruthy()
})

it('rejects with error if not configured correctly', async () => {
client.configure({
apiKey: null
})
await expect(client.notifyAsync(new Error('test'))).rejects.toThrow(new Error('Unable to send error report: no API key has been configured'))
})

it('rejects on pre-condition error', async () => {
client.configure({
apiKey: 'testing',
reportData: false
})

await expect(client.notifyAsync(new Error('test'))).rejects.toThrow(new Error('Dropping notice: honeybadger.js is in development mode'))
})
})

describe('beforeNotify', function () {
beforeEach(function () {
client.configure({
Expand Down
28 changes: 28 additions & 0 deletions test/unit/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,32 @@ describe('server client', function () {
})
})
})

describe('notifyAsync', function () {
beforeEach(() => {
client.configure({
apiKey: 'testing'
})
})

it('resolves after the http request is done', async () => {
const request = nock('https://api.honeybadger.io')
.post('/v1/notices/js')
.reply(201, {
id: '48b98609-dd3b-48ee-bffc-d51f309a2dfa'
})

await client.notifyAsync('testing')
expect(request.isDone()).toBe(true)
})

it('rejects on http error', async () => {
const request = nock('https://api.honeybadger.io')
.post('/v1/notices/js')
.reply(400)

await expect(client.notifyAsync('testing')).rejects.toThrow(/Bad HTTP response/)
expect(request.isDone()).toBe(true)
})
})
})

0 comments on commit b26566a

Please sign in to comment.