@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
|
@@ -4,7 +4,16 @@ import path from 'path'
|
|
|
4
4
|
import { copyConfigFiles } from './copy-config-files';
|
|
5
5
|
describe('copy-config-files', () => {
|
|
6
6
|
|
|
7
|
+
// mock-fs replaces the real filesystem; Jest's BufferedConsole lazily reads
|
|
8
|
+
// jest-util from disk when logging. Under pnpm that path lives deep in
|
|
9
|
+
// node_modules/.pnpm and isn't present in the mock, so console.log throws.
|
|
10
|
+
// The production log isn't under test — stub it while the fs is mocked.
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.spyOn(console, 'log').mockImplementation(() => undefined)
|
|
13
|
+
})
|
|
14
|
+
|
|
7
15
|
afterEach(() => {
|
|
16
|
+
jest.restoreAllMocks()
|
|
8
17
|
mock.restore()
|
|
9
18
|
})
|
|
10
19
|
|
package/src/edge.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './with-honeybadger'
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import Honeybadger from '@honeybadger-io/js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Edge-safe equivalents of the inbound instrumentation helpers in
|
|
5
|
+
* `@honeybadger-io/js` (src/server/instrumentation/http_event.ts). They are
|
|
6
|
+
* duplicated here because this module must also load on the edge runtime where
|
|
7
|
+
* Node builtins (the `crypto` module, `process.hrtime`) are unavailable. Keep
|
|
8
|
+
* the header names and the `request_id` / `correlation_id` contract in sync
|
|
9
|
+
* with that file.
|
|
10
|
+
*
|
|
11
|
+
* Both request shapes Next.js uses are supported: the `*RequestEventContext` /
|
|
12
|
+
* `*RequestEvent` pairs come in a web-`Headers`/`Request` variant (App Router
|
|
13
|
+
* route handlers and middleware) and a Node-bag variant (Pages Router API
|
|
14
|
+
* routes, which only ever run on the Node runtime).
|
|
15
|
+
*/
|
|
16
|
+
function generateId(): string {
|
|
17
|
+
const webCrypto = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto
|
|
18
|
+
if (webCrypto && typeof webCrypto.randomUUID === 'function') {
|
|
19
|
+
try {
|
|
20
|
+
return webCrypto.randomUUID()
|
|
21
|
+
}
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
23
|
+
catch (error) {
|
|
24
|
+
// fall through to manual generation
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// v4-shaped, not crypto-quality. Acceptable since this is a correlation id,
|
|
28
|
+
// not a security token.
|
|
29
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (ch) => {
|
|
30
|
+
const r = (Math.random() * 16) | 0
|
|
31
|
+
const v = ch === 'x' ? r : (r & 0x3) | 0x8
|
|
32
|
+
return v.toString(16)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readHeader(headers: Headers, name: string): string | undefined {
|
|
37
|
+
const value = headers.get(name)
|
|
38
|
+
if (typeof value !== 'string') {
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
const trimmed = value.trim()
|
|
42
|
+
return trimmed.length ? trimmed : undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Node-style headers bag, as seen on the Pages Router `req` (an
|
|
46
|
+
// `IncomingMessage`): lowercased keys, values that may be arrays.
|
|
47
|
+
export type NodeHeaders = Record<string, string | string[] | undefined>
|
|
48
|
+
|
|
49
|
+
// Minimal shape of a Node-style request (Pages Router `NextApiRequest`),
|
|
50
|
+
// covering only what the instrumentation reads.
|
|
51
|
+
export type NodeRequestLike = { method?: string; url?: string; headers: NodeHeaders }
|
|
52
|
+
|
|
53
|
+
function readNodeHeader(headers: NodeHeaders, name: string): string | undefined {
|
|
54
|
+
if (!headers) {
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
const lower = name.toLowerCase()
|
|
58
|
+
let value: string | string[] | undefined = headers[lower]
|
|
59
|
+
if (value === undefined) {
|
|
60
|
+
for (const key of Object.keys(headers)) {
|
|
61
|
+
if (key.toLowerCase() === lower) {
|
|
62
|
+
value = headers[key]
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
value = value[0]
|
|
69
|
+
}
|
|
70
|
+
if (typeof value !== 'string') {
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
73
|
+
const trimmed = value.trim()
|
|
74
|
+
return trimmed.length ? trimmed : undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A type alias (not an interface) so it stays assignable to the
|
|
78
|
+
// Record<string, unknown> that setEventContext expects.
|
|
79
|
+
export type RequestIds = {
|
|
80
|
+
request_id: string
|
|
81
|
+
correlation_id: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Shared id precedence. Kept in one place (rather than once per request shape)
|
|
85
|
+
// so the header-name contract documented above is only spelled out once.
|
|
86
|
+
function seedIds(read: (name: string) => string | undefined): RequestIds {
|
|
87
|
+
const requestId =
|
|
88
|
+
read('x-request-id') ??
|
|
89
|
+
read('request-id') ??
|
|
90
|
+
generateId()
|
|
91
|
+
const correlationId =
|
|
92
|
+
read('x-correlation-id') ??
|
|
93
|
+
read('x-amzn-trace-id') ??
|
|
94
|
+
requestId
|
|
95
|
+
return { request_id: requestId, correlation_id: correlationId }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// App Router / middleware: headers are a web `Headers` instance.
|
|
99
|
+
export function seedRequestEventContext(headers: Headers): RequestIds {
|
|
100
|
+
return seedIds((name) => readHeader(headers, name))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Pages Router: headers are a Node bag (Pages routes are Node-only, never edge).
|
|
104
|
+
export function seedNodeRequestEventContext(headers: NodeHeaders): RequestIds {
|
|
105
|
+
return seedIds((name) => readNodeHeader(headers, name))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function now(): number {
|
|
109
|
+
return typeof performance !== 'undefined' ? performance.now() : Date.now()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Mirrors Util.resolveInsights from @honeybadger-io/core: the master gate and
|
|
113
|
+
// the per-source flag must both be on.
|
|
114
|
+
export function insightsHttpEnabled(): boolean {
|
|
115
|
+
const insights = Honeybadger.config.insights
|
|
116
|
+
return insights?.enabled === true && insights?.http === true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The ids are embedded directly in the payload (instead of relying on the
|
|
120
|
+
// store's eventContext merge) so the event carries them even on the edge
|
|
121
|
+
// runtime, where there is no per-request store isolation. On the Node.js
|
|
122
|
+
// runtime they match the seeded event context, so embedding is a no-op.
|
|
123
|
+
function emitHandledEvent(method: string | undefined, path: string | undefined, status: number | undefined, start: number, ids: RequestIds): void {
|
|
124
|
+
const payload: Record<string, unknown> = {
|
|
125
|
+
method,
|
|
126
|
+
duration: Math.round(now() - start),
|
|
127
|
+
...ids,
|
|
128
|
+
}
|
|
129
|
+
if (typeof path === 'string') {
|
|
130
|
+
payload.path = path
|
|
131
|
+
}
|
|
132
|
+
if (typeof status === 'number') {
|
|
133
|
+
payload.status = status
|
|
134
|
+
}
|
|
135
|
+
Honeybadger.event('request.handled', payload)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// App Router / middleware: `req.url` is absolute, so parse out the pathname.
|
|
139
|
+
export function emitRequestEvent(req: Request, status: number | undefined, start: number, ids: RequestIds): void {
|
|
140
|
+
let path: string | undefined
|
|
141
|
+
try {
|
|
142
|
+
path = new URL(req.url).pathname
|
|
143
|
+
}
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
145
|
+
catch (error) {
|
|
146
|
+
// relative or malformed URL — leave path unset
|
|
147
|
+
}
|
|
148
|
+
emitHandledEvent(req.method, path, status, start, ids)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Pages Router: `req.url` is a relative path that may carry a query string.
|
|
152
|
+
export function emitNodeRequestEvent(req: NodeRequestLike, status: number | undefined, start: number, ids: RequestIds): void {
|
|
153
|
+
const path = typeof req.url === 'string' ? req.url.split('?')[0] : undefined
|
|
154
|
+
emitHandledEvent(req.method, path, status, start, ids)
|
|
155
|
+
}
|