@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.
@@ -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
+ }