@honeybadger-io/nextjs
Version:
Next.js integration for Honeybadger
251 lines (247 loc) • 10.6 kB
JavaScript
import fs from 'fs';
import path from 'path';
import HoneybadgerSourceMapPlugin from '@honeybadger-io/webpack';
import Honeybadger from '@honeybadger-io/js';
const URL_DOCS_SOURCE_MAPS_UPLOAD = 'https://docs.honeybadger.io/lib/javascript/integration/nextjs/#source-map-upload-and-tracking-deploys';
let _silent = true;
function log(type, msg) {
if (['error', 'warn'].includes(type) || !_silent) {
console[type]('[HoneybadgerNextJs]', msg);
}
}
function shouldUploadSourceMaps(honeybadgerNextJsConfig, context) {
const { dev } = context;
if (honeybadgerNextJsConfig.disableSourceMapUpload) {
return false;
}
if (!honeybadgerNextJsConfig.webpackPluginOptions || !honeybadgerNextJsConfig.webpackPluginOptions.apiKey) {
log('warn', `skipping source map upload; here's how to enable: ${URL_DOCS_SOURCE_MAPS_UPLOAD}`);
return false;
}
if (dev || process.env.NODE_ENV === 'development') {
return false;
}
return true;
}
function mergeWithExistingWebpackConfig(nextJsWebpackConfig, honeybadgerNextJsConfig) {
return function webpackFunctionMergedWithHb(webpackConfig, context) {
const { isServer, dir: projectDir, nextRuntime } = context;
const configType = isServer ? (nextRuntime === 'edge' ? 'edge' : 'server') : 'browser';
log('debug', `reached webpackFunctionMergedWithHb isServer[${isServer}] configType[${configType}]`);
let result = { ...webpackConfig };
if (typeof nextJsWebpackConfig === 'function') {
result = nextJsWebpackConfig(result, context);
}
const originalEntry = result.entry;
result.entry = async () => injectHoneybadgerConfigToEntry(originalEntry, projectDir, configType);
if (shouldUploadSourceMaps(honeybadgerNextJsConfig, context)) {
// `result.devtool` must be 'hidden-source-map' or 'source-map' to properly pass sourcemaps.
// Next.js uses regular `source-map` which doesnt pass its sourcemaps to Webpack.
// https://github.com/vercel/next.js/blob/89ec21ed686dd79a5770b5c669abaff8f55d8fef/packages/next/build/webpack/config/blocks/base.ts#L40
// Use the hidden-source-map option when you don't want the source maps to be
// publicly available on the servers, only to the error reporting
result.devtool = 'hidden-source-map';
if (!result.plugins) {
result.plugins = [];
}
const options = getWebpackPluginOptions(honeybadgerNextJsConfig);
if (options) {
result.plugins.push(new HoneybadgerSourceMapPlugin(options));
}
}
return result;
};
}
async function injectHoneybadgerConfigToEntry(originalEntry, projectDir, configType) {
const result = typeof originalEntry === 'function' ? await originalEntry() : { ...originalEntry };
const hbConfigFile = getHoneybadgerConfigFile(projectDir, configType);
if (!hbConfigFile) {
return result;
}
const hbConfigFileRelativePath = `./${hbConfigFile}`;
if (!Object.keys(result).length) {
log('debug', `no entry points for configType[${configType}]`);
}
for (const entryName in result) {
addHoneybadgerConfigToEntry(result, entryName, hbConfigFileRelativePath, configType);
}
return result;
}
function addHoneybadgerConfigToEntry(entry, entryName, hbConfigFile, configType) {
log('debug', `adding entry[${entryName}] to configType[${configType}]`);
switch (configType) {
case 'server':
if (!entryName.startsWith('pages/')) {
return;
}
break;
case 'browser':
if (!['pages/_app', 'main-app'].includes(entryName)) {
return;
}
break;
}
const currentEntryPoint = entry[entryName];
let newEntryPoint = currentEntryPoint;
if (typeof currentEntryPoint === 'string') {
newEntryPoint = [hbConfigFile, currentEntryPoint];
}
else if (Array.isArray(currentEntryPoint)) {
newEntryPoint = [hbConfigFile, ...currentEntryPoint];
} // descriptor object (webpack 5+)
else if (typeof currentEntryPoint === 'object' && currentEntryPoint && 'import' in currentEntryPoint) {
const currentImportValue = currentEntryPoint['import'];
const newImportValue = [hbConfigFile];
if (typeof currentImportValue === 'string') {
newImportValue.push(currentImportValue);
}
else {
newImportValue.push(...(currentImportValue));
}
newEntryPoint = {
...currentEntryPoint,
import: newImportValue,
};
}
else {
log('error', 'Could not inject Honeybadger config to entry point: ' + JSON.stringify(currentEntryPoint, null, 2));
}
entry[entryName] = newEntryPoint;
}
function getHoneybadgerConfigFile(projectDir, configType) {
const possibilities = [`honeybadger.${configType}.config.ts`, `honeybadger.${configType}.config.js`];
for (const filename of possibilities) {
if (fs.existsSync(path.resolve(projectDir, filename))) {
return filename;
}
}
log('debug', `could not find config file in ${projectDir} for ${configType}`);
return null;
}
function getWebpackPluginOptions(honeybadgerNextJsConfig) {
var _a, _b, _c;
const apiKey = ((_a = honeybadgerNextJsConfig.webpackPluginOptions) === null || _a === void 0 ? void 0 : _a.apiKey) || process.env.NEXT_PUBLIC_HONEYBADGER_API_KEY;
const assetsUrl = ((_b = honeybadgerNextJsConfig.webpackPluginOptions) === null || _b === void 0 ? void 0 : _b.assetsUrl) || process.env.NEXT_PUBLIC_HONEYBADGER_ASSETS_URL;
if (!apiKey || !assetsUrl) {
log('error', 'Missing Honeybadger required configuration for webpack plugin. Source maps will not be uploaded to Honeybadger.');
return null;
}
return {
...honeybadgerNextJsConfig.webpackPluginOptions,
apiKey,
assetsUrl,
revision: ((_c = honeybadgerNextJsConfig.webpackPluginOptions) === null || _c === void 0 ? void 0 : _c.revision) || process.env.NEXT_PUBLIC_HONEYBADGER_REVISION,
silent: _silent,
};
}
function getNextJsVersionInstalled() {
var _a;
try {
return (_a = require('next/package.json').version) === null || _a === void 0 ? void 0 : _a.split('.');
}
catch (e) {
return null;
}
}
/**
* NextJs will report a warning if the `serverExternalPackages` option is not present.
* This is because @honeybadger-io/js will try to require configuration files dynamically (https://github.com/honeybadger-io/honeybadger-js/pull/1268).
*
* First reported here: https://github.com/honeybadger-io/honeybadger-js/issues/1351
*/
function addServerExternalPackagesOption(config) {
var _a, _b;
// this should be available in the upcoming version of Next.js (14.3.0)
if (config.serverExternalPackages && Array.isArray(config.serverExternalPackages)) {
log('debug', 'adding @honeybadger-io/js to serverExternalPackages');
config.serverExternalPackages.push('@honeybadger-io/js');
return;
}
if (((_a = config.experimental) === null || _a === void 0 ? void 0 : _a.serverComponentsExternalPackages) && Array.isArray((_b = config.experimental) === null || _b === void 0 ? void 0 : _b.serverComponentsExternalPackages)) {
log('debug', 'adding @honeybadger-io/js to experimental.serverComponentsExternalPackages');
config.experimental.serverComponentsExternalPackages.push('@honeybadger-io/js');
return;
}
const nextJsVersion = getNextJsVersionInstalled();
if (nextJsVersion) {
if ((+nextJsVersion[0] === 14 && +nextJsVersion[1] >= 3) || +nextJsVersion[0] > 14) {
log('debug', 'adding serverExternalPackages option with value ["@honeybadger-io/js"]');
config.serverExternalPackages = ['@honeybadger-io/js'];
}
else {
log('debug', 'adding experimental.serverComponentsExternalPackages option with value ["@honeybadger-io/js"]');
if (!config.experimental) {
config.experimental = {};
}
config.experimental.serverComponentsExternalPackages = ['@honeybadger-io/js'];
}
}
}
function setupHoneybadger(config, honeybadgerNextJsConfig) {
var _a;
if (!honeybadgerNextJsConfig) {
honeybadgerNextJsConfig = {
silent: true,
disableSourceMapUpload: false,
};
}
_silent = (_a = honeybadgerNextJsConfig.silent) !== null && _a !== void 0 ? _a : true;
addServerExternalPackagesOption(config);
return {
...config,
webpack: mergeWithExistingWebpackConfig(config.webpack, honeybadgerNextJsConfig)
};
}
function configure() {
var _a;
if (((_a = Honeybadger.config.apiKey) === null || _a === void 0 ? void 0 : _a.length) > 0) {
return;
}
let projectRoot = undefined;
try {
// not available on edge runtime
projectRoot = process.cwd();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
catch (error) {
// do nothing
}
Honeybadger
.configure({
apiKey: process.env.NEXT_PUBLIC_HONEYBADGER_API_KEY,
environment: process.env.NEXT_PUBLIC_VERCEL_ENV || process.env.VERCEL_ENV || process.env.NODE_ENV,
revision: process.env.NEXT_PUBLIC_HONEYBADGER_REVISION,
projectRoot: 'webpack://_N_E/./'
})
.beforeNotify((notice) => {
if (!projectRoot) {
return;
}
notice === null || notice === void 0 ? void 0 : notice.backtrace.forEach((line) => {
if (line.file) {
line.file = line.file.replace(`${projectRoot}/.next/server`, `${process.env.NEXT_PUBLIC_HONEYBADGER_ASSETS_URL}/..`);
}
return line;
});
});
}
/**
* Wraps a handler function with Honeybadger error reporting.
* Use with Next.js API route handlers or middleware.
*/
function withHoneybadger(handler) {
configure();
return new Proxy(handler, {
apply: async (target, thisArg, args) => {
try {
return await Reflect.apply(target, thisArg, args);
}
catch (error) {
await Honeybadger.notifyAsync(error);
throw error; // Re-throw the error after reporting it
}
},
});
}
export { setupHoneybadger, withHoneybadger };
//# sourceMappingURL=honeybadger-nextjs.esm.js.map