@honeybadger-io/nextjs 5.10.13 → 5.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/edge.d.ts +2 -0
- package/dist/honeybadger-nextjs-edge.cjs.js +384 -0
- package/dist/honeybadger-nextjs-edge.cjs.js.map +1 -0
- package/dist/honeybadger-nextjs-edge.esm.js +357 -0
- package/dist/honeybadger-nextjs-edge.esm.js.map +1 -0
- package/dist/honeybadger-nextjs.cjs.js +334 -14
- package/dist/honeybadger-nextjs.cjs.js.map +1 -1
- package/dist/honeybadger-nextjs.esm.js +315 -14
- package/dist/honeybadger-nextjs.esm.js.map +1 -1
- package/dist/insights-instrumentation.d.ts +17 -0
- package/dist/with-honeybadger.d.ts +28 -3
- package/dist/with-honeybadger.test.d.ts +2 -0
- package/package.json +33 -6
- package/src/copy-config-files.test.ts +9 -0
- package/src/edge.ts +1 -0
- package/src/insights-instrumentation.ts +155 -0
- package/src/with-honeybadger.test.ts +527 -0
- package/src/with-honeybadger.ts +242 -12
- package/templates/honeybadger.browser.config.js +4 -2
- package/templates/honeybadger.edge.config.js +4 -2
- package/templates/honeybadger.server.config.js +11 -8
package/src/with-honeybadger.ts
CHANGED
|
@@ -1,7 +1,86 @@
|
|
|
1
1
|
import Honeybadger from '@honeybadger-io/js';
|
|
2
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import * as nextServer from 'next/server';
|
|
4
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
5
|
+
import {
|
|
6
|
+
emitNodeRequestEvent,
|
|
7
|
+
emitRequestEvent,
|
|
8
|
+
insightsHttpEnabled,
|
|
9
|
+
now,
|
|
10
|
+
seedNodeRequestEventContext,
|
|
11
|
+
seedRequestEventContext,
|
|
12
|
+
} from './insights-instrumentation';
|
|
13
|
+
import type { NodeRequestLike } from './insights-instrumentation';
|
|
3
14
|
|
|
4
|
-
|
|
15
|
+
type WaitUntil = (promise: Promise<unknown>) => void
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The `waitUntil` primitive the hosting platform injects per request. Next.js
|
|
19
|
+
* resolves `after()` through this same accessor, and it is the only channel
|
|
20
|
+
* available in Pages Router API routes, which are invoked as `(req, res)` with
|
|
21
|
+
* no context argument to read it from.
|
|
22
|
+
*/
|
|
23
|
+
function requestContextWaitUntil(): WaitUntil | undefined {
|
|
24
|
+
const context = (globalThis as Record<symbol, unknown>)[Symbol.for('@next/request-context')] as
|
|
25
|
+
| { get?: () => { waitUntil?: WaitUntil } | undefined }
|
|
26
|
+
| undefined
|
|
27
|
+
const waitUntil = context?.get?.()?.waitUntil
|
|
28
|
+
return typeof waitUntil === 'function' ? waitUntil : undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Middleware receives a `NextFetchEvent` as its second argument. Duck-typed
|
|
33
|
+
* rather than `instanceof` so the edge bundle needs no runtime import, and
|
|
34
|
+
* bound because `waitUntil` is a class method that collects into the event.
|
|
35
|
+
*/
|
|
36
|
+
function eventWaitUntil(event: unknown): WaitUntil | undefined {
|
|
37
|
+
const waitUntil = (event as { waitUntil?: WaitUntil } | undefined)?.waitUntil
|
|
38
|
+
return typeof waitUntil === 'function' ? waitUntil.bind(event) : undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Ensure Insights events are delivered before the serverless/edge runtime
|
|
43
|
+
* freezes, without delaying the response where the runtime lets us avoid it.
|
|
44
|
+
*
|
|
45
|
+
* In order of preference: Next.js `after()` (stable in 15.1, App Router only —
|
|
46
|
+
* it needs App Router request context, so Pages Router must not call it), then
|
|
47
|
+
* a `waitUntil` from the middleware event or the platform request context,
|
|
48
|
+
* then a blocking `flushAsync()` when the runtime offers neither. Blocking is
|
|
49
|
+
* correct in that last case: no `waitUntil` means nothing is going to freeze
|
|
50
|
+
* the invocation out from under us.
|
|
51
|
+
*
|
|
52
|
+
* Delivery failures are logged by the events worker and must not break the handler.
|
|
53
|
+
*/
|
|
54
|
+
function scheduleFlush(options: { useAfter?: boolean; waitUntil?: WaitUntil } = {}): Promise<void> | void {
|
|
55
|
+
const flush = () => Honeybadger.flushAsync().catch(() => { /* logged by the events worker */ })
|
|
56
|
+
|
|
57
|
+
if (options.useAfter) {
|
|
58
|
+
const after = (nextServer as { after?: (cb: () => unknown) => void }).after
|
|
59
|
+
if (typeof after === 'function') {
|
|
60
|
+
// Exported but still refusable: `after()` throws outside a supported
|
|
61
|
+
// context. Fall through to the remaining strategies rather than failing
|
|
62
|
+
// the request.
|
|
63
|
+
try {
|
|
64
|
+
after(flush)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
68
|
+
catch (error) {
|
|
69
|
+
// try waitUntil / blocking flush below
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const waitUntil = options.waitUntil ?? requestContextWaitUntil()
|
|
75
|
+
if (waitUntil) {
|
|
76
|
+
waitUntil(flush())
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return flush()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function configure(overrides?: Parameters<typeof Honeybadger.configure>[0]) {
|
|
5
84
|
if (Honeybadger.config.apiKey?.length > 0) {
|
|
6
85
|
return;
|
|
7
86
|
}
|
|
@@ -21,7 +100,8 @@ function configure() {
|
|
|
21
100
|
apiKey: process.env.NEXT_PUBLIC_HONEYBADGER_API_KEY,
|
|
22
101
|
environment: process.env.NEXT_PUBLIC_VERCEL_ENV || process.env.VERCEL_ENV || process.env.NODE_ENV,
|
|
23
102
|
revision: process.env.NEXT_PUBLIC_HONEYBADGER_REVISION,
|
|
24
|
-
projectRoot: 'webpack://_N_E/./'
|
|
103
|
+
projectRoot: 'webpack://_N_E/./',
|
|
104
|
+
...overrides,
|
|
25
105
|
})
|
|
26
106
|
.beforeNotify((notice) => {
|
|
27
107
|
if (!projectRoot) {
|
|
@@ -38,19 +118,169 @@ function configure() {
|
|
|
38
118
|
}
|
|
39
119
|
|
|
40
120
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
121
|
+
* Next.js uses thrown errors for control flow: `redirect()`, `notFound()`,
|
|
122
|
+
* `forbidden()` and `unauthorized()` all throw an error carrying a `digest`
|
|
123
|
+
* string (`NEXT_REDIRECT;...`, `NEXT_NOT_FOUND`, `NEXT_HTTP_ERROR_FALLBACK;...`).
|
|
124
|
+
* These are not real failures — the framework catches them upstream to produce
|
|
125
|
+
* the redirect/404/etc. — so we must let them propagate without reporting them,
|
|
126
|
+
* otherwise every redirect shows up as an error in Honeybadger.
|
|
127
|
+
*
|
|
128
|
+
* We match on the `NEXT_` prefix rather than an exhaustive list so that any
|
|
129
|
+
* present or future framework control-flow digest is covered. This is safe:
|
|
130
|
+
* genuine errors that React tags with a `digest` use an opaque hash, and other
|
|
131
|
+
* Next.js bailout signals (e.g. `BAILOUT_TO_CLIENT_SIDE_RENDERING`,
|
|
132
|
+
* `DYNAMIC_SERVER_USAGE`) are not `NEXT_`-prefixed, so neither is skipped.
|
|
133
|
+
*/
|
|
134
|
+
function isNextControlFlowError(error: unknown): boolean {
|
|
135
|
+
const digest = (error as { digest?: unknown } | null | undefined)?.digest
|
|
136
|
+
return typeof digest === 'string' && digest.startsWith('NEXT_')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
type AppRouterHandler = (req: NextRequest | Request, ...args: unknown[]) => Promise<NextResponse>
|
|
140
|
+
type PagesApiHandler = (req: NextApiRequest, res: NextApiResponse, ...args: unknown[]) => unknown
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Detects a Pages Router API invocation: `(req, res)` where `res` is a Node
|
|
144
|
+
* `ServerResponse`. We branch on this structurally because — unlike an App
|
|
145
|
+
* Router route handler — there is no returned `Response` to read the status
|
|
146
|
+
* from; it lives on `res.statusCode`.
|
|
147
|
+
*/
|
|
148
|
+
function isPagesApiInvocation(args: unknown[]): args is [NodeRequestLike, NextApiResponse] {
|
|
149
|
+
const req = args[0] as { headers?: unknown } | undefined
|
|
150
|
+
const res = args[1] as { statusCode?: unknown; end?: unknown } | undefined
|
|
151
|
+
return (
|
|
152
|
+
!!req && typeof req.headers === 'object' && req.headers !== null &&
|
|
153
|
+
!!res && typeof res.statusCode === 'number' && typeof res.end === 'function'
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* App Router route handlers and middleware: a web `Request`/`NextRequest` in, a
|
|
159
|
+
* `Response`/`NextResponse` out. The status comes from the returned response.
|
|
160
|
+
*
|
|
161
|
+
* `waitUntil` is present for middleware (from its `NextFetchEvent`); route
|
|
162
|
+
* handlers get `{ params }` as their second argument and rely on `after()`.
|
|
163
|
+
*/
|
|
164
|
+
async function handleAppRouterRequest(call: () => unknown, req: Request, canIsolate: boolean, waitUntil?: WaitUntil): Promise<unknown> {
|
|
165
|
+
const ids = seedRequestEventContext(req.headers)
|
|
166
|
+
if (canIsolate) {
|
|
167
|
+
Honeybadger.setEventContext(ids)
|
|
168
|
+
}
|
|
169
|
+
const start = insightsHttpEnabled() ? now() : null
|
|
170
|
+
try {
|
|
171
|
+
const response = await call()
|
|
172
|
+
if (start !== null) {
|
|
173
|
+
emitRequestEvent(req, (response as Response | undefined)?.status, start, ids)
|
|
174
|
+
await scheduleFlush({ useAfter: true, waitUntil })
|
|
175
|
+
}
|
|
176
|
+
return response
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (isNextControlFlowError(error)) {
|
|
179
|
+
throw error
|
|
180
|
+
}
|
|
181
|
+
if (start !== null) {
|
|
182
|
+
emitRequestEvent(req, 500, start, ids)
|
|
183
|
+
await scheduleFlush({ useAfter: true, waitUntil })
|
|
184
|
+
}
|
|
185
|
+
await Honeybadger.notifyAsync(error as Error)
|
|
186
|
+
throw error
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Pages Router API routes: a Node `req`/`res` pair. The handler writes to `res`
|
|
192
|
+
* and returns nothing meaningful, so the final status is read from
|
|
193
|
+
* `res.statusCode` once it resolves.
|
|
194
|
+
*/
|
|
195
|
+
async function handlePagesApiRequest(call: () => unknown, req: NodeRequestLike, res: NextApiResponse, canIsolate: boolean): Promise<unknown> {
|
|
196
|
+
const ids = seedNodeRequestEventContext(req.headers)
|
|
197
|
+
if (canIsolate) {
|
|
198
|
+
Honeybadger.setEventContext(ids)
|
|
199
|
+
}
|
|
200
|
+
const start = insightsHttpEnabled() ? now() : null
|
|
201
|
+
try {
|
|
202
|
+
const result = await call()
|
|
203
|
+
if (start !== null) {
|
|
204
|
+
emitNodeRequestEvent(req, res.statusCode, start, ids)
|
|
205
|
+
// No after() here: Pages Router lacks the App Router request context it
|
|
206
|
+
// needs. scheduleFlush falls through to the platform waitUntil instead.
|
|
207
|
+
await scheduleFlush({ useAfter: false })
|
|
208
|
+
}
|
|
209
|
+
return result
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (isNextControlFlowError(error)) {
|
|
212
|
+
throw error
|
|
213
|
+
}
|
|
214
|
+
if (start !== null) {
|
|
215
|
+
emitNodeRequestEvent(req, 500, start, ids)
|
|
216
|
+
await scheduleFlush({ useAfter: false })
|
|
217
|
+
}
|
|
218
|
+
await Honeybadger.notifyAsync(error as Error)
|
|
219
|
+
throw error
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Unrecognised invocation shape: still report errors, but emit no insights
|
|
225
|
+
* event since we can't reliably read the request.
|
|
226
|
+
*/
|
|
227
|
+
async function handleUninstrumented(call: () => unknown): Promise<unknown> {
|
|
228
|
+
try {
|
|
229
|
+
return await call()
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (isNextControlFlowError(error)) {
|
|
232
|
+
throw error
|
|
233
|
+
}
|
|
234
|
+
await Honeybadger.notifyAsync(error as Error)
|
|
235
|
+
throw error
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Wraps a handler function with Honeybadger error reporting. Works with App
|
|
241
|
+
* Router route handlers, middleware, and Pages Router API routes.
|
|
242
|
+
*
|
|
243
|
+
* `request_id` / `correlation_id` are read from the `x-request-id` /
|
|
244
|
+
* `request-id` and `x-correlation-id` / `x-amzn-trace-id` headers (generated
|
|
245
|
+
* when absent). When `insights: { enabled: true, http: true }` is configured,
|
|
246
|
+
* a `request.handled` event carrying the ids plus method, path, status and
|
|
247
|
+
* duration is emitted per request.
|
|
248
|
+
*
|
|
249
|
+
* On the Node.js runtime each invocation additionally runs inside
|
|
250
|
+
* `Honeybadger.run(...)`, so context is isolated per request and the ids are
|
|
251
|
+
* seeded onto the event context — merged onto every event emitted during the
|
|
252
|
+
* request, including programmatic `Honeybadger.event(...)` calls. On the edge
|
|
253
|
+
* runtime (browser build, single global store) seeding the shared event
|
|
254
|
+
* context would leak ids between concurrent requests, so programmatic events
|
|
255
|
+
* there don't inherit them.
|
|
256
|
+
*
|
|
257
|
+
* The webpack config-file auto-injection (`honeybadger.*.config.js`) doesn't
|
|
258
|
+
* reach API routes or edge middleware, so pass `config` to configure
|
|
259
|
+
* Honeybadger explicitly there. It's ignored if Honeybadger is already
|
|
260
|
+
* configured (e.g. by the auto-injected file).
|
|
43
261
|
*/
|
|
44
|
-
export function withHoneybadger(handler:
|
|
45
|
-
|
|
262
|
+
export function withHoneybadger(handler: AppRouterHandler, config?: Parameters<typeof Honeybadger.configure>[0]): AppRouterHandler
|
|
263
|
+
export function withHoneybadger(handler: PagesApiHandler, config?: Parameters<typeof Honeybadger.configure>[0]): PagesApiHandler
|
|
264
|
+
export function withHoneybadger(handler: AppRouterHandler | PagesApiHandler, config?: Parameters<typeof Honeybadger.configure>[0]) {
|
|
265
|
+
configure(config);
|
|
46
266
|
return new Proxy(handler, {
|
|
47
|
-
apply:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
267
|
+
apply: (target, thisArg, args) => {
|
|
268
|
+
const canIsolate = typeof Honeybadger.run === 'function'
|
|
269
|
+
const call = () => Reflect.apply(target, thisArg, args)
|
|
270
|
+
|
|
271
|
+
const invoke = (): unknown => {
|
|
272
|
+
// App Router / middleware first: a web Request as the first argument.
|
|
273
|
+
if (typeof Request !== 'undefined' && args[0] instanceof Request) {
|
|
274
|
+
return handleAppRouterRequest(call, args[0], canIsolate, eventWaitUntil(args[1]))
|
|
275
|
+
}
|
|
276
|
+
// Pages Router API route: a Node req/res pair.
|
|
277
|
+
if (isPagesApiInvocation(args)) {
|
|
278
|
+
return handlePagesApiRequest(call, args[0], args[1], canIsolate)
|
|
279
|
+
}
|
|
280
|
+
return handleUninstrumented(call)
|
|
53
281
|
}
|
|
282
|
+
|
|
283
|
+
return canIsolate ? Honeybadger.run(invoke) : invoke()
|
|
54
284
|
},
|
|
55
285
|
});
|
|
56
286
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Honeybadger } from '@honeybadger-io/react'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
export const config = {
|
|
4
4
|
apiKey: process.env.NEXT_PUBLIC_HONEYBADGER_API_KEY,
|
|
5
5
|
environment: process.env.NEXT_PUBLIC_VERCEL_ENV || process.env.VERCEL_ENV || process.env.NODE_ENV,
|
|
6
6
|
revision: process.env.NEXT_PUBLIC_HONEYBADGER_REVISION,
|
|
7
7
|
projectRoot: 'webpack://_N_E/./',
|
|
8
8
|
// debug: true,
|
|
9
9
|
// reportData: true,
|
|
10
|
-
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
Honeybadger.configure(config)
|
|
11
13
|
Honeybadger.logger.debug('Honeybadger configured for browser')
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import Honeybadger from '@honeybadger-io/js'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
export const config = {
|
|
4
4
|
apiKey: process.env.NEXT_PUBLIC_HONEYBADGER_API_KEY,
|
|
5
5
|
environment: process.env.NEXT_PUBLIC_VERCEL_ENV || process.env.VERCEL_ENV || process.env.NODE_ENV,
|
|
6
6
|
revision: process.env.NEXT_PUBLIC_HONEYBADGER_REVISION,
|
|
7
7
|
projectRoot: 'webpack://_N_E/./',
|
|
8
8
|
// debug: true,
|
|
9
9
|
// reportData: true,
|
|
10
|
-
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
Honeybadger.configure(config)
|
|
11
13
|
Honeybadger.logger.debug('Honeybadger configured for edge')
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import Honeybadger from '@honeybadger-io/js'
|
|
2
2
|
|
|
3
3
|
const projectRoot = process.cwd()
|
|
4
|
+
|
|
5
|
+
export const config = {
|
|
6
|
+
apiKey: process.env.NEXT_PUBLIC_HONEYBADGER_API_KEY,
|
|
7
|
+
environment: process.env.NEXT_PUBLIC_VERCEL_ENV || process.env.VERCEL_ENV || process.env.NODE_ENV,
|
|
8
|
+
revision: process.env.NEXT_PUBLIC_HONEYBADGER_REVISION,
|
|
9
|
+
projectRoot: 'webpack:///./',
|
|
10
|
+
// debug: true,
|
|
11
|
+
// reportData: true,
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
Honeybadger
|
|
5
|
-
.configure(
|
|
6
|
-
apiKey: process.env.NEXT_PUBLIC_HONEYBADGER_API_KEY,
|
|
7
|
-
environment: process.env.NEXT_PUBLIC_VERCEL_ENV || process.env.VERCEL_ENV || process.env.NODE_ENV,
|
|
8
|
-
revision: process.env.NEXT_PUBLIC_HONEYBADGER_REVISION,
|
|
9
|
-
projectRoot: 'webpack:///./',
|
|
10
|
-
// debug: true,
|
|
11
|
-
// reportData: true,
|
|
12
|
-
})
|
|
15
|
+
.configure(config)
|
|
13
16
|
.beforeNotify((notice) => {
|
|
14
17
|
if (!notice) {
|
|
15
18
|
return
|