Skip to content

Commit e610760

Browse files
authored
feat(nextjs): withHoneybadger wrapper for route handlers and middleware (#1435)
1 parent 0fb9be8 commit e610760

File tree

5 files changed

+291
-231
lines changed

5 files changed

+291
-231
lines changed

packages/nextjs/src/index.ts

Lines changed: 2 additions & 228 deletions
Original file line numberDiff line numberDiff line change
@@ -1,228 +1,2 @@
1-
import fs from 'fs'
2-
import path from 'path'
3-
import HoneybadgerSourceMapPlugin from '@honeybadger-io/webpack'
4-
import type { WebpackConfigContext } from 'next/dist/server/config-shared'
5-
import { HoneybadgerNextJsConfig, NextJsRuntime, HoneybadgerWebpackPluginOptions } from './types'
6-
7-
const URL_DOCS_SOURCE_MAPS_UPLOAD = 'https://docs.honeybadger.io/lib/javascript/integration/nextjs/#source-map-upload-and-tracking-deploys'
8-
let _silent = true
9-
function log(type: 'error' | 'warn' | 'debug', msg: string): void {
10-
if (['error', 'warn'].includes(type) || !_silent) {
11-
console[type]('[HoneybadgerNextJs]', msg)
12-
}
13-
}
14-
15-
function shouldUploadSourceMaps(honeybadgerNextJsConfig: HoneybadgerNextJsConfig, context: WebpackConfigContext): boolean {
16-
const { dev } = context
17-
18-
if (honeybadgerNextJsConfig.disableSourceMapUpload) {
19-
return false
20-
}
21-
22-
if (!honeybadgerNextJsConfig.webpackPluginOptions || !honeybadgerNextJsConfig.webpackPluginOptions.apiKey) {
23-
log('warn', `skipping source map upload; here's how to enable: ${URL_DOCS_SOURCE_MAPS_UPLOAD}`)
24-
return false
25-
}
26-
27-
if (dev || process.env.NODE_ENV === 'development') {
28-
return false
29-
}
30-
31-
return true
32-
}
33-
34-
function mergeWithExistingWebpackConfig(nextJsWebpackConfig, honeybadgerNextJsConfig: HoneybadgerNextJsConfig) {
35-
return function webpackFunctionMergedWithHb(webpackConfig, context: WebpackConfigContext) {
36-
37-
const { isServer, dir: projectDir, nextRuntime } = context
38-
const configType = isServer ? (nextRuntime === 'edge' ? 'edge' : 'server') : 'browser'
39-
log('debug', `reached webpackFunctionMergedWithHb isServer[${isServer}] configType[${configType}]`)
40-
41-
let result = { ...webpackConfig }
42-
if (typeof nextJsWebpackConfig === 'function') {
43-
result = nextJsWebpackConfig(result, context)
44-
}
45-
46-
const originalEntry = result.entry
47-
result.entry = async () => injectHoneybadgerConfigToEntry(originalEntry, projectDir, configType)
48-
49-
if (shouldUploadSourceMaps(honeybadgerNextJsConfig, context)) {
50-
// `result.devtool` must be 'hidden-source-map' or 'source-map' to properly pass sourcemaps.
51-
// Next.js uses regular `source-map` which doesnt pass its sourcemaps to Webpack.
52-
// https://github.com/vercel/next.js/blob/89ec21ed686dd79a5770b5c669abaff8f55d8fef/packages/next/build/webpack/config/blocks/base.ts#L40
53-
// Use the hidden-source-map option when you don't want the source maps to be
54-
// publicly available on the servers, only to the error reporting
55-
result.devtool = 'hidden-source-map'
56-
if (!result.plugins) {
57-
result.plugins = []
58-
}
59-
const options = getWebpackPluginOptions(honeybadgerNextJsConfig)
60-
if (options) {
61-
result.plugins.push(new HoneybadgerSourceMapPlugin(options))
62-
}
63-
}
64-
65-
return result
66-
}
67-
}
68-
69-
async function injectHoneybadgerConfigToEntry(originalEntry, projectDir: string, configType: NextJsRuntime) {
70-
const result = typeof originalEntry === 'function' ? await originalEntry() : { ...originalEntry }
71-
const hbConfigFile = getHoneybadgerConfigFile(projectDir, configType)
72-
if (!hbConfigFile) {
73-
return result
74-
}
75-
76-
const hbConfigFileRelativePath = `./${hbConfigFile}`
77-
if (!Object.keys(result).length) {
78-
log('debug', `no entry points for configType[${configType}]`)
79-
}
80-
for (const entryName in result) {
81-
addHoneybadgerConfigToEntry(result, entryName, hbConfigFileRelativePath, configType)
82-
}
83-
84-
return result
85-
}
86-
87-
function addHoneybadgerConfigToEntry(entry, entryName: string, hbConfigFile: string, configType: NextJsRuntime) {
88-
89-
log('debug', `adding entry[${entryName}] to configType[${configType}]`)
90-
91-
switch (configType) {
92-
case 'server':
93-
if (!entryName.startsWith('pages/')) {
94-
return
95-
}
96-
97-
break
98-
case 'browser':
99-
if (!['pages/_app', 'main-app'].includes(entryName)) {
100-
return
101-
}
102-
103-
break
104-
case 'edge':
105-
// nothing?
106-
107-
break
108-
}
109-
110-
const currentEntryPoint = entry[entryName]
111-
let newEntryPoint = currentEntryPoint
112-
113-
if (typeof currentEntryPoint === 'string') {
114-
newEntryPoint = [hbConfigFile, currentEntryPoint]
115-
} else if (Array.isArray(currentEntryPoint)) {
116-
newEntryPoint = [hbConfigFile, ...currentEntryPoint]
117-
} // descriptor object (webpack 5+)
118-
else if (typeof currentEntryPoint === 'object' && currentEntryPoint && 'import' in currentEntryPoint) {
119-
const currentImportValue = currentEntryPoint['import']
120-
const newImportValue = [hbConfigFile]
121-
if (typeof currentImportValue === 'string') {
122-
newImportValue.push(currentImportValue)
123-
} else {
124-
newImportValue.push(...(currentImportValue))
125-
}
126-
newEntryPoint = {
127-
...currentEntryPoint,
128-
import: newImportValue,
129-
};
130-
} else {
131-
log('error', 'Could not inject Honeybadger config to entry point: ' + JSON.stringify(currentEntryPoint, null, 2))
132-
}
133-
134-
entry[entryName] = newEntryPoint
135-
}
136-
137-
function getHoneybadgerConfigFile(projectDir: string, configType: NextJsRuntime): string | null {
138-
const possibilities = [`honeybadger.${configType}.config.ts`, `honeybadger.${configType}.config.js`]
139-
140-
for (const filename of possibilities) {
141-
if (fs.existsSync(path.resolve(projectDir, filename))) {
142-
return filename;
143-
}
144-
}
145-
146-
log('debug', `could not find config file in ${projectDir} for ${configType}`)
147-
return null
148-
}
149-
150-
function getWebpackPluginOptions(honeybadgerNextJsConfig: HoneybadgerNextJsConfig): HoneybadgerWebpackPluginOptions | null {
151-
const apiKey = honeybadgerNextJsConfig.webpackPluginOptions?.apiKey || process.env.NEXT_PUBLIC_HONEYBADGER_API_KEY
152-
const assetsUrl = honeybadgerNextJsConfig.webpackPluginOptions?.assetsUrl || process.env.NEXT_PUBLIC_HONEYBADGER_ASSETS_URL
153-
if (!apiKey || !assetsUrl) {
154-
log('error', 'Missing Honeybadger required configuration for webpack plugin. Source maps will not be uploaded to Honeybadger.')
155-
156-
return null
157-
}
158-
159-
return {
160-
...honeybadgerNextJsConfig.webpackPluginOptions,
161-
apiKey,
162-
assetsUrl,
163-
revision: honeybadgerNextJsConfig.webpackPluginOptions?.revision || process.env.NEXT_PUBLIC_HONEYBADGER_REVISION,
164-
silent: _silent,
165-
}
166-
}
167-
168-
function getNextJsVersionInstalled(): [major: string, minor: string, patch: string] | null {
169-
try {
170-
return require('next/package.json').version?.split('.')
171-
} catch (e) {
172-
return null
173-
}
174-
}
175-
176-
/**
177-
* NextJs will report a warning if the `serverExternalPackages` option is not present.
178-
* This is because @honeybadger-io/js will try to require configuration files dynamically (https://github.com/honeybadger-io/honeybadger-js/pull/1268).
179-
*
180-
* First reported here: https://github.com/honeybadger-io/honeybadger-js/issues/1351
181-
*/
182-
function addServerExternalPackagesOption(config) {
183-
// this should be available in the upcoming version of Next.js (14.3.0)
184-
if (config.serverExternalPackages && Array.isArray(config.serverExternalPackages)) {
185-
log('debug', 'adding @honeybadger-io/js to serverExternalPackages')
186-
config.serverExternalPackages.push('@honeybadger-io/js')
187-
return
188-
}
189-
190-
if (config.experimental?.serverComponentsExternalPackages && Array.isArray(config.experimental?.serverComponentsExternalPackages)) {
191-
log('debug', 'adding @honeybadger-io/js to experimental.serverComponentsExternalPackages')
192-
config.experimental.serverComponentsExternalPackages.push('@honeybadger-io/js')
193-
return
194-
}
195-
196-
const nextJsVersion = getNextJsVersionInstalled();
197-
if (nextJsVersion) {
198-
if ((+nextJsVersion[0] === 14 && +nextJsVersion[1] >= 3) || +nextJsVersion[0] > 14) {
199-
log('debug', 'adding serverExternalPackages option with value ["@honeybadger-io/js"]')
200-
config.serverExternalPackages = ['@honeybadger-io/js']
201-
}
202-
else {
203-
log('debug', 'adding experimental.serverComponentsExternalPackages option with value ["@honeybadger-io/js"]')
204-
if (!config.experimental) {
205-
config.experimental = {}
206-
}
207-
config.experimental.serverComponentsExternalPackages = ['@honeybadger-io/js']
208-
}
209-
}
210-
}
211-
212-
export function setupHoneybadger(config, honeybadgerNextJsConfig?: HoneybadgerNextJsConfig) {
213-
if (!honeybadgerNextJsConfig) {
214-
honeybadgerNextJsConfig = {
215-
silent: true,
216-
disableSourceMapUpload: false,
217-
}
218-
}
219-
220-
_silent = honeybadgerNextJsConfig.silent ?? true
221-
222-
addServerExternalPackagesOption(config)
223-
224-
return {
225-
...config,
226-
webpack: mergeWithExistingWebpackConfig(config.webpack, honeybadgerNextJsConfig)
227-
}
228-
}
1+
export * from './webpack'
2+
export * from './with-honeybadger'

0 commit comments

Comments
 (0)