|
| 1 | +import * as Sentry from '@sentry/node'; |
| 2 | +import * as utils from '@sentry/utils'; |
| 3 | +import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'; |
| 4 | + |
| 5 | +import { AugmentedNextApiResponse, withSentry, WrappedNextApiHandler } from '../../src/utils/withSentry'; |
| 6 | + |
| 7 | +const FLUSH_DURATION = 200; |
| 8 | + |
| 9 | +async function sleep(ms: number): Promise<void> { |
| 10 | + await new Promise(resolve => setTimeout(resolve, ms)); |
| 11 | +} |
| 12 | + |
| 13 | +async function callWrappedHandler(wrappedHandler: WrappedNextApiHandler, req: NextApiRequest, res: NextApiResponse) { |
| 14 | + await wrappedHandler(req, res); |
| 15 | + |
| 16 | + // Within the wrapped handler, we await `flush()` inside `res.end()`, but nothing awaits `res.end()`, because in its |
| 17 | + // original version, it's sync (unlike the function we wrap around it). The original does actually *act* like an async |
| 18 | + // function being awaited - subsequent steps in the request/response lifecycle don't happen until it emits a |
| 19 | + // `prefinished` event (which is why it's safe for us to make the switch). But here in tests, there's nothing past |
| 20 | + // `res.end()`, so we have to manually wait for it to be done. |
| 21 | + await sleep(FLUSH_DURATION); |
| 22 | + while (!res.finished) { |
| 23 | + await sleep(100); |
| 24 | + } |
| 25 | +} |
| 26 | + |
| 27 | +// We mock `captureException` as a no-op because under normal circumstances it is an un-awaited effectively-async |
| 28 | +// function which might or might not finish before any given test ends, potentially leading jest to error out. |
| 29 | +const captureExceptionSpy = jest.spyOn(Sentry, 'captureException').mockImplementation(jest.fn()); |
| 30 | +const loggerSpy = jest.spyOn(utils.logger, 'log'); |
| 31 | +const flushSpy = jest.spyOn(Sentry, 'flush').mockImplementation(async () => { |
| 32 | + // simulate the time it takes time to flush all events |
| 33 | + await sleep(FLUSH_DURATION); |
| 34 | + return true; |
| 35 | +}); |
| 36 | + |
| 37 | +describe('withSentry', () => { |
| 38 | + let req: NextApiRequest, res: NextApiResponse; |
| 39 | + |
| 40 | + const error = new Error('Oh, no! Charlie ate the flip-flops! :-('); |
| 41 | + |
| 42 | + const origHandlerNoError: NextApiHandler = async (_req, res) => { |
| 43 | + res.send('Good dog, Maisey!'); |
| 44 | + }; |
| 45 | + const origHandlerWithError: NextApiHandler = async (_req, _res) => { |
| 46 | + throw error; |
| 47 | + }; |
| 48 | + |
| 49 | + const wrappedHandlerNoError = withSentry(origHandlerNoError); |
| 50 | + const wrappedHandlerWithError = withSentry(origHandlerWithError); |
| 51 | + |
| 52 | + beforeEach(() => { |
| 53 | + req = { url: 'http://dogs.are.great' } as NextApiRequest; |
| 54 | + res = ({ |
| 55 | + send: function(this: AugmentedNextApiResponse) { |
| 56 | + this.end(); |
| 57 | + }, |
| 58 | + end: function(this: AugmentedNextApiResponse) { |
| 59 | + this.finished = true; |
| 60 | + }, |
| 61 | + } as unknown) as AugmentedNextApiResponse; |
| 62 | + }); |
| 63 | + |
| 64 | + afterEach(() => { |
| 65 | + jest.clearAllMocks(); |
| 66 | + }); |
| 67 | + |
| 68 | + describe('flushing', () => { |
| 69 | + it('flushes events before rethrowing error', async () => { |
| 70 | + try { |
| 71 | + await callWrappedHandler(wrappedHandlerWithError, req, res); |
| 72 | + } catch (err) { |
| 73 | + expect(err).toBe(error); |
| 74 | + } |
| 75 | + |
| 76 | + expect(captureExceptionSpy).toHaveBeenCalledWith(error); |
| 77 | + expect(flushSpy).toHaveBeenCalled(); |
| 78 | + expect(loggerSpy).toHaveBeenCalledWith('Done flushing events'); |
| 79 | + |
| 80 | + // This ensures the expect inside the `catch` block actually ran, i.e., that in the end the wrapped handler |
| 81 | + // errored out the same way it would without sentry, meaning the error was indeed rethrown |
| 82 | + expect.assertions(4); |
| 83 | + }); |
| 84 | + |
| 85 | + it('flushes events before finishing non-erroring response', async () => { |
| 86 | + await callWrappedHandler(wrappedHandlerNoError, req, res); |
| 87 | + |
| 88 | + expect(flushSpy).toHaveBeenCalled(); |
| 89 | + expect(loggerSpy).toHaveBeenCalledWith('Done flushing events'); |
| 90 | + }); |
| 91 | + }); |
| 92 | +}); |
0 commit comments