diff --git a/packages/plugin-core/test/sourcemaps.test.ts b/packages/plugin-core/test/sourcemaps.test.ts index c5668c3e7..0b4355464 100644 --- a/packages/plugin-core/test/sourcemaps.test.ts +++ b/packages/plugin-core/test/sourcemaps.test.ts @@ -3,7 +3,7 @@ import FormData from 'form-data' import { Buffer } from 'buffer' import { Response, FetchError } from 'node-fetch' -describe('hbUtils', () => { +describe('sourcemaps', () => { const fetchMock = td.func() const hbOptions = { endpoint: 'https://honeybadger.io/api/sourcemaps/test', diff --git a/packages/webpack/example/README.md b/packages/webpack/example/README.md index 8f947ad68..e529b88e8 100644 --- a/packages/webpack/example/README.md +++ b/packages/webpack/example/README.md @@ -5,7 +5,11 @@ Webpack project based [Webpack's example project](https://webpack.js.org/guides/ Note that currently this project is just used to test the upload of sourcemaps and deploy notifications -- the sourcemaps are not expected to be correctly applied to errors from this project since we didn't set up the revision and assetsUrl. ## Setup -Your API key should be in an environment variable `HONEYBADGER_API_KEY`. You can do this with [direnv](https://direnv.net/) or however you like. +Two environment variables are required: +1. `HONEYBADGER_API_KEY`: Your project's API key +2. `HONEYBADGER_REVISION`: A unique string. This needs to match between your Honeybadger.configure() and the sourcemap plugin for sourcemaps to be applied. + +You can manage environment variables with [direnv](https://direnv.net/) or however you like. ## Testing Run `npm run build` to run webpack. You should see a sourcemap and a deploy notification uploaded to Honeybadger. \ No newline at end of file diff --git a/packages/webpack/example/index.html b/packages/webpack/example/index.html index 90375db88..e33142571 100644 --- a/packages/webpack/example/index.html +++ b/packages/webpack/example/index.html @@ -5,6 +5,6 @@ Getting Started - + \ No newline at end of file diff --git a/packages/webpack/example/package-lock.json b/packages/webpack/example/package-lock.json index 3e2ce2453..440e7bdd0 100644 --- a/packages/webpack/example/package-lock.json +++ b/packages/webpack/example/package-lock.json @@ -24,12 +24,7 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.14.5", - "fetch-retry": "^5.0.3", - "form-data": "^4.0.0", - "isomorphic-fetch": "^3.0.0", - "lodash.find": "^4.3.0", - "lodash.foreach": "^4.2.0", - "lodash.reduce": "^4.3.0", + "@honeybadger-io/plugin-core": "^1.1.0", "verror": "^1.6.1" }, "devDependencies": { @@ -41,7 +36,7 @@ "chai": "^4.3.4", "cross-env": "^7.0.0", "debug": "^4.1.0", - "lodash": "^4.17.21", + "lodash.find": "^4.6.0", "mocha": "^9.0.1", "nock": "^13.1.0", "rimraf": "^3.0.2", @@ -1541,16 +1536,11 @@ "@babel/preset-env": "^7.14.5", "@babel/register": "^7.14.5", "@babel/runtime": "^7.14.5", + "@honeybadger-io/plugin-core": "^1.1.0", "chai": "^4.3.4", "cross-env": "^7.0.0", "debug": "^4.1.0", - "fetch-retry": "^5.0.3", - "form-data": "^4.0.0", - "isomorphic-fetch": "^3.0.0", - "lodash": "^4.17.21", - "lodash.find": "^4.3.0", - "lodash.foreach": "^4.2.0", - "lodash.reduce": "^4.3.0", + "lodash.find": "^4.6.0", "mocha": "^9.0.1", "nock": "^13.1.0", "rimraf": "^3.0.2", diff --git a/packages/webpack/example/src/hb.js b/packages/webpack/example/src/hb.js new file mode 100644 index 000000000..a3972cdba --- /dev/null +++ b/packages/webpack/example/src/hb.js @@ -0,0 +1,8 @@ +const Honeybadger = require('@honeybadger-io/js'); + +Honeybadger.configure({ + apiKey: process.env.HONEYBADGER_API_KEY, + revision: process.env.HONEYBADGER_REVISION, +}) + +export default Honeybadger \ No newline at end of file diff --git a/packages/webpack/example/src/index.js b/packages/webpack/example/src/index.js index caa515a7a..7493ae745 100644 --- a/packages/webpack/example/src/index.js +++ b/packages/webpack/example/src/index.js @@ -1,8 +1,4 @@ -const Honeybadger = require('@honeybadger-io/js'); - -Honeybadger.configure({ - apiKey: (prompt('Enter the API key for your Honeybadger project:')), -}) +import Honeybadger from './hb' function notifyButton() { const button = document.createElement('button'); diff --git a/packages/webpack/example/webpack.config.js b/packages/webpack/example/webpack.config.js index c53e29a64..c29df8d6a 100644 --- a/packages/webpack/example/webpack.config.js +++ b/packages/webpack/example/webpack.config.js @@ -1,18 +1,35 @@ -const path = require('path'); +const path = require('path') const HoneybadgerSourceMapPlugin = require('@honeybadger-io/webpack') +const { EnvironmentPlugin } = require('webpack') module.exports = { - entry: './src/index.js', + // Entry here would normally just be the `index.js` file, however + // adding multiple entry points generates multiple output files, + // allowing us to test multiple source map uploads + entry: { + index: './src/index.js', + hb: './src/hb.js', + }, mode: 'production', output: { - filename: 'index.js', + filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), }, devtool: 'source-map', - plugins: [new HoneybadgerSourceMapPlugin({ - apiKey: process.env.HONEYBADGER_API_KEY, - assetsUrl: 'https://cdn.example.com/assets', - revision: 'main', - deploy: true - })] + plugins: [ + new HoneybadgerSourceMapPlugin({ + apiKey: process.env.HONEYBADGER_API_KEY, + // assetsUrl would normally be a url where your assets are hosted + // This is just to test locally, so it's a folder path instead + assetsUrl: '*/dist', + revision: process.env.HONEYBADGER_REVISION, + deploy: { + environment: 'test', + localUsername: 'hbTestUser', + } + }), + // Be aware that if you use EnvironmentPlugin, the values of those env variables + // get bundled into your code as strings. + new EnvironmentPlugin(['HONEYBADGER_API_KEY', 'HONEYBADGER_REVISION']) + ] }; \ No newline at end of file diff --git a/packages/webpack/package-lock.json b/packages/webpack/package-lock.json index d87a5c737..86dbf39b8 100644 --- a/packages/webpack/package-lock.json +++ b/packages/webpack/package-lock.json @@ -10,12 +10,7 @@ "license": "MIT", "dependencies": { "@babel/runtime": "^7.14.5", - "fetch-retry": "^5.0.3", - "form-data": "^4.0.0", - "isomorphic-fetch": "^3.0.0", - "lodash.find": "^4.3.0", - "lodash.foreach": "^4.2.0", - "lodash.reduce": "^4.3.0", + "@honeybadger-io/plugin-core": "^1.1.0", "verror": "^1.6.1" }, "devDependencies": { @@ -27,7 +22,7 @@ "chai": "^4.3.4", "cross-env": "^7.0.0", "debug": "^4.1.0", - "lodash": "^4.17.21", + "lodash.find": "^4.6.0", "mocha": "^9.0.1", "nock": "^13.1.0", "rimraf": "^3.0.2", @@ -1710,6 +1705,16 @@ "node": ">=6.9.0" } }, + "node_modules/@honeybadger-io/plugin-core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@honeybadger-io/plugin-core/-/plugin-core-1.1.0.tgz", + "integrity": "sha512-xYck7KhCOM+6sgyL+n+6Q/eXimzNkT8fpRK3HGB7g6pQe87geF1quh2n+9W+Z/6f/wEaJGiyd0wzVUc3DFOBhA==", + "dependencies": { + "fetch-retry": "^5.0.3", + "form-data": "^4.0.0", + "node-fetch": "^2.6.9" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", @@ -3120,34 +3125,6 @@ "node": ">=0.10.0" } }, - "node_modules/isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "dependencies": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - } - }, - "node_modules/isomorphic-fetch/node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -3300,12 +3277,8 @@ "node_modules/lodash.find": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", - "integrity": "sha512-yaRZoAV3Xq28F1iafWN1+a0rflOej93l1DQUejs3SZ41h2O9UJBoS9aueGjPDgAl4B6tPC0NuuchLKaDQQ3Isg==" - }, - "node_modules/lodash.foreach": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==" + "integrity": "sha512-yaRZoAV3Xq28F1iafWN1+a0rflOej93l1DQUejs3SZ41h2O9UJBoS9aueGjPDgAl4B6tPC0NuuchLKaDQQ3Isg==", + "dev": true }, "node_modules/lodash.get": { "version": "4.4.2", @@ -3313,11 +3286,6 @@ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, - "node_modules/lodash.reduce": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", - "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==" - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -3427,9 +3395,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -3688,6 +3656,25 @@ "node": ">= 10.13" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", @@ -4119,9 +4106,9 @@ } }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -4578,11 +4565,6 @@ "node": ">=10.13.0" } }, - "node_modules/whatwg-fetch": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", - "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" - }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -5890,6 +5872,16 @@ "to-fast-properties": "^2.0.0" } }, + "@honeybadger-io/plugin-core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@honeybadger-io/plugin-core/-/plugin-core-1.1.0.tgz", + "integrity": "sha512-xYck7KhCOM+6sgyL+n+6Q/eXimzNkT8fpRK3HGB7g6pQe87geF1quh2n+9W+Z/6f/wEaJGiyd0wzVUc3DFOBhA==", + "requires": { + "fetch-retry": "^5.0.3", + "form-data": "^4.0.0", + "node-fetch": "^2.6.9" + } + }, "@jridgewell/gen-mapping": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", @@ -7000,25 +6992,6 @@ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true }, - "isomorphic-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", - "requires": { - "node-fetch": "^2.6.1", - "whatwg-fetch": "^3.4.1" - }, - "dependencies": { - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "requires": { - "whatwg-url": "^5.0.0" - } - } - } - }, "jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -7134,12 +7107,8 @@ "lodash.find": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", - "integrity": "sha512-yaRZoAV3Xq28F1iafWN1+a0rflOej93l1DQUejs3SZ41h2O9UJBoS9aueGjPDgAl4B6tPC0NuuchLKaDQQ3Isg==" - }, - "lodash.foreach": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==" + "integrity": "sha512-yaRZoAV3Xq28F1iafWN1+a0rflOej93l1DQUejs3SZ41h2O9UJBoS9aueGjPDgAl4B6tPC0NuuchLKaDQQ3Isg==", + "dev": true }, "lodash.get": { "version": "4.4.2", @@ -7147,11 +7116,6 @@ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, - "lodash.reduce": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", - "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==" - }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -7233,9 +7197,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } @@ -7435,6 +7399,14 @@ "propagate": "^2.0.0" } }, + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, "node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", @@ -7755,9 +7727,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true }, "serialize-javascript": { @@ -8072,11 +8044,6 @@ "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", "dev": true }, - "whatwg-fetch": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", - "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" - }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/packages/webpack/package.json b/packages/webpack/package.json index f0770a6b6..166e1c6cb 100644 --- a/packages/webpack/package.json +++ b/packages/webpack/package.json @@ -42,7 +42,7 @@ "chai": "^4.3.4", "cross-env": "^7.0.0", "debug": "^4.1.0", - "lodash": "^4.17.21", + "lodash.find": "^4.6.0", "mocha": "^9.0.1", "nock": "^13.1.0", "rimraf": "^3.0.2", @@ -52,12 +52,7 @@ }, "dependencies": { "@babel/runtime": "^7.14.5", - "fetch-retry": "^5.0.3", - "form-data": "^4.0.0", - "isomorphic-fetch": "^3.0.0", - "lodash.find": "^4.3.0", - "lodash.foreach": "^4.2.0", - "lodash.reduce": "^4.3.0", + "@honeybadger-io/plugin-core": "^1.1.0", "verror": "^1.6.1" }, "peerDependencies": { diff --git a/packages/webpack/src/HoneybadgerSourceMapPlugin.js b/packages/webpack/src/HoneybadgerSourceMapPlugin.js index af6c393ba..80af00be6 100644 --- a/packages/webpack/src/HoneybadgerSourceMapPlugin.js +++ b/packages/webpack/src/HoneybadgerSourceMapPlugin.js @@ -1,16 +1,8 @@ -import { promises as fs } from 'fs' import { join } from 'path' -import originalFetch from 'isomorphic-fetch'; -import fetchRetry from 'fetch-retry'; -import VError from 'verror' -import find from 'lodash.find' -import reduce from 'lodash.reduce' -import FormData from 'form-data' -import { handleError, validateOptions } from './helpers' -import { ENDPOINT, DEPLOY_ENDPOINT, PLUGIN_NAME, MAX_RETRIES, MIN_WORKER_COUNT } from './constants' -import { resolvePromiseWithWorkers } from './resolvePromiseWithWorkers' +import { handleError } from './helpers' +import { cleanOptions, sendDeployNotification, uploadSourcemaps } from '@honeybadger-io/plugin-core' -const fetch = fetchRetry(originalFetch) +const PLUGIN_NAME = 'HoneybadgerSourceMapPlugin' /** * @typedef {Object} DeployObject @@ -20,56 +12,30 @@ const fetch = fetchRetry(originalFetch) */ class HoneybadgerSourceMapPlugin { - constructor ({ - apiKey, - assetsUrl, - endpoint = ENDPOINT, - revision = 'main', - silent = false, - ignoreErrors = false, - retries = 3, - workerCount = 5, - deploy = false - }) { - this.apiKey = apiKey - this.assetsUrl = assetsUrl - this.endpoint = endpoint - this.revision = revision - this.silent = silent - this.ignoreErrors = ignoreErrors - this.workerCount = Math.max(workerCount, MIN_WORKER_COUNT) - /** @type DeployObject|boolean */ - this.deploy = deploy - this.retries = retries - - if (this.retries > MAX_RETRIES) { - this.retries = MAX_RETRIES - } + constructor (options) { + this.sendDeployNotification = sendDeployNotification + this.uploadSourceMaps = uploadSourcemaps + this.options = cleanOptions(options) } async afterEmit (compilation) { if (this.isDevServerRunning()) { - if (!this.silent) { + if (!this.options.silent) { console.info('\nHoneybadgerSourceMapPlugin will not upload source maps because webpack-dev-server is running.') } - - return - } - - const errors = validateOptions(this) - - if (errors) { - compilation.errors.push(...handleError(errors)) return } try { - await this.uploadSourceMaps(compilation) - await this.sendDeployNotification() + const assets = this.getAssets(compilation) + await this.uploadSourceMaps(assets, this.options) + if (this.options.deploy) { + await this.sendDeployNotification(this.options) + } } catch (err) { - if (!this.ignoreErrors) { + if (!this.options.ignoreErrors) { compilation.errors.push(...handleError(err)) - } else if (!this.silent) { + } else if (!this.options.silent) { compilation.warnings.push(...handleError(err)) } } @@ -83,216 +49,29 @@ class HoneybadgerSourceMapPlugin { compiler.hooks.afterEmit.tapPromise(PLUGIN_NAME, this.afterEmit.bind(this)) } - // eslint-disable-next-line class-methods-use-this getAssetPath (compilation, name) { + if (!name) { return '' } return join( compilation.getPath(compilation.compiler.outputPath), name.split('?')[0] ) } - getSource (compilation, name) { - const path = this.getAssetPath(compilation, name) - return fs.readFile(path, { encoding: 'utf-8' }) - } - - getAssets (compilation) { + getAssets(compilation) { const { chunks } = compilation.getStats().toJson() - - return reduce(chunks, (result, chunk) => { - const sourceFile = find(chunk.files, file => /\.js$/.test(file)) - - // Webpack 4 using chunk.files, Webpack 5 uses chunk.auxiliaryFiles - // https://webpack.js.org/blog/2020-10-10-webpack-5-release/#stats - const sourceMap = (chunk.auxiliaryFiles || chunk.files).find(file => - /\.js\.map$/.test(file) - ) - - if (!sourceFile || !sourceMap) { - return result - } - - return [ - ...result, - { sourceFile, sourceMap } - ] - }, []) - } - - getUrlToAsset (sourceFile) { - if (typeof sourceFile === 'string') { - const sep = '/' - const unsanitized = `${this.assetsUrl}${sep}${sourceFile}` - return unsanitized.replace(/([^:]\/)\/+/g, '$1') - } - return this.assetsUrl(sourceFile) - } - - async uploadSourceMap (compilation, { sourceFile, sourceMap }) { - const errorMessage = `failed to upload ${sourceMap} to Honeybadger API` - - let sourceMapSource - let sourceFileSource - - try { - sourceMapSource = await this.getSource(compilation, sourceMap) - sourceFileSource = await this.getSource(compilation, sourceFile) - } catch (err) { - throw new VError(err, err.message) - } - - const form = new FormData() - form.append('api_key', this.apiKey) - form.append('minified_url', this.getUrlToAsset(sourceFile)) - form.append('minified_file', sourceFileSource, { - filename: sourceFile, - contentType: 'application/javascript' - }) - form.append('source_map', sourceMapSource, { - filename: sourceMap, - contentType: 'application/octet-stream' - }) - form.append('revision', this.revision) - - let res - try { - res = await fetch(this.endpoint, { - method: 'POST', - body: form, - redirect: 'follow', - retries: this.retries, - retryDelay: 1000 + return chunks + .map(({ files, auxiliaryFiles }) => { + const jsFilename = files.find(file => /\.js$/.test(file)) + const jsFilePath = this.getAssetPath(compilation, jsFilename) + // Webpack 4 using chunk.files, Webpack 5 uses chunk.auxiliaryFiles + // https://webpack.js.org/blog/2020-10-10-webpack-5-release/#stats + const sourcemapFilename = (auxiliaryFiles || files).find(file => + /\.js\.map$/.test(file) + ) + const sourcemapFilePath = this.getAssetPath(compilation, sourcemapFilename) + return { sourcemapFilename, sourcemapFilePath, jsFilename, jsFilePath } }) - } catch (err) { - // network / operational errors. Does not include 404 / 500 errors - throw new VError(err, errorMessage) - } - - // >= 400 responses - if (!res.ok) { - // Attempt to parse error details from response - let details - try { - const body = await res.json() - - if (body && body.error) { - details = body.error - } else { - details = `${res.status} - ${res.statusText}` - } - } catch (parseErr) { - details = `${res.status} - ${res.statusText}` - } - - throw new Error(`${errorMessage}: ${details}`) - } - - // Success - if (!this.silent) { - // eslint-disable-next-line no-console - console.info(`Uploaded ${sourceMap} to Honeybadger API`) - } - } - - uploadSourceMaps (compilation) { - const assets = this.getAssets(compilation) - - if (assets.length <= 0) { - // We should probably tell people they're not uploading assets. - // this is also an open issue on Rollbar sourcemap plugin - // https://github.com/thredup/rollbar-sourcemap-webpack-plugin/issues/39 - if (!this.silent) { - console.info(this.noAssetsFoundMessage) - } - - return - } - - console.info('\n') - - // On large projects source maps should not all be uploaded at the same time, - // but in parallel with a reasonable worker count in order to avoid network issues - return resolvePromiseWithWorkers( - assets.map(asset => () => this.uploadSourceMap(compilation, asset)), - this.workerCount - ) - } - - async sendDeployNotification () { - if (this.deploy === false || this.apiKey == null) { - return - } - - let body - - if (this.deploy === true) { - body = { - deploy: { - revision: this.revision - } - } - } else if (typeof this.deploy === 'object' && this.deploy !== null) { - body = { - deploy: { - revision: this.revision, - repository: this.deploy.repository, - local_username: this.deploy.localUsername, - environment: this.deploy.environment - } - } - } - - const errorMessage = 'Unable to send deploy notification to Honeybadger API.' - let res - - try { - res = await fetch(DEPLOY_ENDPOINT, { - method: 'POST', - headers: { - 'X-API-KEY': this.apiKey, - 'Content-Type': 'application/json', - Accept: 'application/json' - }, - body: JSON.stringify(body), - redirect: 'follow', - retries: this.retries, - retryDelay: 1000 - }) - } catch (err) { - // network / operational errors. Does not include 404 / 500 errors - if (!this.ignoreErrors) { - throw new VError(err, errorMessage) - } - } - - // >= 400 responses - if (!res.ok) { - // Attempt to parse error details from response - let details - try { - const body = await res.json() - - if (body && body.error) { - details = body.error - } else { - details = `${res.status} - ${res.statusText}` - } - } catch (parseErr) { - details = `${res.status} - ${res.statusText}` - } - - if (!this.ignoreErrors) { - throw new Error(`${errorMessage}: ${details}`) - } - } - - if (!this.silent) { - console.info('Successfully sent deploy notification to Honeybadger API.') - } - } - - get noAssetsFoundMessage () { - return '\nHoneybadger could not find any sourcemaps. Nothing will be uploaded.' + .filter(({ sourcemapFilename, jsFilename }) => sourcemapFilename && jsFilename) } } diff --git a/packages/webpack/src/constants.js b/packages/webpack/src/constants.js deleted file mode 100644 index 0fa3e6a09..000000000 --- a/packages/webpack/src/constants.js +++ /dev/null @@ -1,10 +0,0 @@ -export const PLUGIN_NAME = 'HoneybadgerSourceMapPlugin' -export const ENDPOINT = 'https://api.honeybadger.io/v1/source_maps' -export const DEPLOY_ENDPOINT = 'https://api.honeybadger.io/v1/deploys' -export const MAX_RETRIES = 10 -export const MIN_WORKER_COUNT = 1 - -export const REQUIRED_FIELDS = [ - 'apiKey', - 'assetsUrl' -] diff --git a/packages/webpack/src/helpers.js b/packages/webpack/src/helpers.js index fd54cf62a..add349b5c 100644 --- a/packages/webpack/src/helpers.js +++ b/packages/webpack/src/helpers.js @@ -1,5 +1,4 @@ import VError from 'verror' -import { REQUIRED_FIELDS } from './constants' export function handleError (err, prefix = 'HoneybadgerSourceMapPlugin') { if (!err) { @@ -10,17 +9,4 @@ export function handleError (err, prefix = 'HoneybadgerSourceMapPlugin') { return errors.map(e => new VError(e, prefix)) } -export function validateOptions (ref) { - const errors = REQUIRED_FIELDS.reduce((result, field) => { - if (ref && ref[field]) { - return result - } - return [ - ...result, - new Error(`required field, '${field}', is missing.`) - ] - }, []) - - return errors.length ? errors : null -} diff --git a/packages/webpack/src/resolvePromiseWithWorkers.js b/packages/webpack/src/resolvePromiseWithWorkers.js deleted file mode 100644 index c9f85037f..000000000 --- a/packages/webpack/src/resolvePromiseWithWorkers.js +++ /dev/null @@ -1,36 +0,0 @@ -function * generator ( - promiseFactories -) { - for (let i = 0; i < promiseFactories.length; ++i) { - yield [promiseFactories[i](), i] - } -} - -async function worker (generator, results) { - for (const [promise, index] of generator) { - results[index] = await promise - } -} - -export async function resolvePromiseWithWorkers ( - promiseFactories, - workerCount -) { - // The generator and the results are shared between workers, ensuring each promise is only resolved once - const sharedGenerator = generator(promiseFactories) - const results = [] - - // There's no need to create more workers than promises to resolve - const actualWorkerCount = Math.min( - workerCount, - promiseFactories.length - ) - - const workers = Array.from(new Array(actualWorkerCount)).map(() => - worker(sharedGenerator, results) - ) - - await Promise.all(workers) - - return results -} diff --git a/packages/webpack/test/HoneybadgerSourceMapPlugin.test.js b/packages/webpack/test/HoneybadgerSourceMapPlugin.test.js index 6459c188b..f33a720cc 100644 --- a/packages/webpack/test/HoneybadgerSourceMapPlugin.test.js +++ b/packages/webpack/test/HoneybadgerSourceMapPlugin.test.js @@ -1,533 +1,268 @@ /* eslint-env mocha */ -import chai from 'chai' +import { expect } from 'chai' import * as sinon from 'sinon' import nock from 'nock' -import { promises as fs } from 'fs' // eslint-disable-next-line import/default import HoneybadgerSourceMapPlugin from '../src/HoneybadgerSourceMapPlugin' -import { ENDPOINT, MAX_RETRIES, MIN_WORKER_COUNT, PLUGIN_NAME } from '../src/constants' - -const expect = chai.expect const TEST_ENDPOINT = 'https://api.honeybadger.io' const SOURCEMAP_PATH = '/v1/source_maps' const DEPLOY_PATH = '/v1/deploys' -describe(PLUGIN_NAME, function () { +describe('HoneybadgerSourceMapPlugin', function () { + let compiler + let plugin + + const options = { + apiKey: 'abcd1234', + assetsUrl: 'https://cdn.example.com/assets', + endpoint: `${TEST_ENDPOINT}${SOURCEMAP_PATH}`, + deployEndpoint: `${TEST_ENDPOINT}${DEPLOY_PATH}`, + } + beforeEach(function () { - this.compiler = { + compiler = { hooks: { - afterEmit: { - tapPromise: sinon.spy() - } + afterEmit: { tapPromise: sinon.spy() } } } + plugin = new HoneybadgerSourceMapPlugin(options) + nock.disableNetConnect() + }) - this.options = { - apiKey: 'abcd1234', - assetsUrl: 'https://cdn.example.com/assets' - } - - this.plugin = new HoneybadgerSourceMapPlugin(this.options) + afterEach(function () { + sinon.restore() + nock.cleanAll() }) describe('constructor', function () { it('should return an instance', function () { - expect(this.plugin).to.be.an.instanceof(HoneybadgerSourceMapPlugin) + expect(plugin).to.be.an.instanceof(HoneybadgerSourceMapPlugin) }) - it('should set options', function () { - const options = Object.assign({}, this.options, { + it('should set options using defaults from plugin-core', function () { + const options = { + ...options, apiKey: 'other-api-key', assetsUrl: 'https://cdn.example.com/assets', endpoint: 'https://my-random-endpoint.com' - }) - const plugin = new HoneybadgerSourceMapPlugin(options) - expect(plugin).to.include(options) - }) - - it('should default silent to false', function () { - expect(this.plugin).to.include({ silent: false }) - }) - - it('should default revision to "main"', function () { - expect(this.plugin).to.include({ revision: 'main' }) - }) - - it('should default retries to 3', function () { - expect(this.plugin).to.include({ retries: 3 }) - }) - - it('should default workerCount to 5', function () { - expect(this.plugin).to.include({ workerCount: 5 }) - }) - - it('should default a minimum worker count', function () { - const plugin = new HoneybadgerSourceMapPlugin({ workerCount: -1 }) - expect(plugin).to.include({ workerCount: MIN_WORKER_COUNT }) - }) - - it('should default endpoint to https://api.honeybadger.io/v1/source_maps', function () { - expect(this.plugin).to.include({ endpoint: ENDPOINT }) - }) - - it('should scale back any retries > 10', function () { - const options = { ...this.options, retries: 40 } - const plugin = new HoneybadgerSourceMapPlugin(options) - expect(plugin).to.include({ retries: MAX_RETRIES }) - }) - - it('should allow users to set retries to 0', function () { - const options = { ...this.options, retries: 0 } + } const plugin = new HoneybadgerSourceMapPlugin(options) - expect(plugin).to.include({ retries: 0 }) + expect(plugin.options).to.deep.equal({ + apiKey: 'other-api-key', + assetsUrl: 'https://cdn.example.com/assets', + deploy: false, + deployEndpoint: 'https://api.honeybadger.io/v1/deploys', + endpoint: 'https://my-random-endpoint.com', + ignoreErrors: false, + ignorePaths: [], + retries: 3, + revision: 'main', + silent: false, + workerCount: 5, + }) }) }) describe('apply', function () { it('should hook into "after-emit"', function () { - this.compiler.plugin = sinon.stub() - this.plugin.apply(this.compiler) + compiler.plugin = sinon.stub() + plugin.apply(compiler) - const tapPromise = this.compiler.hooks.afterEmit.tapPromise + const tapPromise = compiler.hooks.afterEmit.tapPromise expect(tapPromise.callCount).to.eq(1) const compilerArgs = tapPromise.getCall(0).args compilerArgs[1] = compilerArgs[1].toString() expect(compilerArgs).to.include.members([ - PLUGIN_NAME, + 'HoneybadgerSourceMapPlugin', compilerArgs[1] ]) }) }) describe('afterEmit', function () { - afterEach(function () { - sinon.reset() - }) - - it('should call uploadSourceMaps', async function () { - const compilation = { + let compilation + const chunks = [ + { + id: 0, + names: ['app'], + files: ['app.5190.js', 'app.5190.js.map'] + }, + ] + const assets = [{ + sourcemapFilePath: '/fake/output/path/app.5190.js.map', + sourcemapFilename: 'app.5190.js.map', + jsFilePath: '/fake/output/path/app.5190.js', + jsFilename: 'app.5190.js', + }] + const outputPath = '/fake/output/path' + + beforeEach(() => { + compilation = { errors: [], - warnings: [] + warnings: [], + getStats: () => ({ + toJson: () => ({ chunks }) + }), + compiler: { outputPath }, + getPath: () => outputPath, } + }) - sinon.stub(this.plugin, 'uploadSourceMaps') - - await this.plugin.afterEmit(compilation) + it('should call uploadSourceMaps', async function () { + sinon.stub(plugin, 'uploadSourceMaps') - expect(this.plugin.uploadSourceMaps.callCount).to.eq(1) + await plugin.afterEmit(compilation) + expect(plugin.uploadSourceMaps.callCount).to.eq(1) + expect(plugin.uploadSourceMaps.getCall(0).args[0]).to.deep.equal(assets) expect(compilation.errors.length).to.eq(0) expect(compilation.warnings.length).to.eq(0) }) + it('should call sendDeployNotification if deploy is true', async () => { + plugin.options.deploy = true + sinon.stub(plugin, 'uploadSourceMaps') + sinon.stub(plugin, 'sendDeployNotification') + + await plugin.afterEmit(compilation) + expect(plugin.sendDeployNotification.callCount).to.eq(1) + expect(plugin.sendDeployNotification.calledWith(plugin.options)).to.equal(true) + }) + + it('should call sendDeployNotification if deploy is a deploy object', async () => { + plugin.options.deploy = { + environment: 'test', + repository: 'https://cdn.example.com', + localUsername: 'itsMeHi' + } + sinon.stub(plugin, 'uploadSourceMaps') + sinon.stub(plugin, 'sendDeployNotification') + + await plugin.afterEmit(compilation) + expect(plugin.sendDeployNotification.callCount).to.eq(1) + expect(plugin.sendDeployNotification.calledWith(plugin.options)).to.equal(true) + }) + it('should add upload warnings to compilation warnings, ' + 'if ignoreErrors is true and silent is false', async function () { - const compilation = { - errors: [], - warnings: [] - } - this.plugin.ignoreErrors = true - this.plugin.silent = false + plugin.options.ignoreErrors = true + plugin.options.silent = false - sinon.stub(this.plugin, 'uploadSourceMaps') + sinon.stub(plugin, 'uploadSourceMaps') .callsFake(() => { throw new Error() }) - await this.plugin.afterEmit(compilation) + await plugin.afterEmit(compilation) - expect(this.plugin.uploadSourceMaps.callCount).to.eq(1) + expect(plugin.uploadSourceMaps.callCount).to.eq(1) expect(compilation.errors.length).to.eq(0) expect(compilation.warnings.length).to.eq(1) expect(compilation.warnings[0]).to.be.an.instanceof(Error) }) it('should not add upload errors to compilation warnings if silent is true', async function () { - const compilation = { - errors: [], - warnings: [] - } - this.plugin.ignoreErrors = true - this.plugin.silent = true + plugin.options.ignoreErrors = true + plugin.options.silent = true - sinon.stub(this.plugin, 'uploadSourceMaps') + sinon.stub(plugin, 'uploadSourceMaps') .callsFake(() => { throw new Error() }) - await this.plugin.afterEmit(compilation) + await plugin.afterEmit(compilation) - expect(this.plugin.uploadSourceMaps.callCount).to.eq(1) + expect(plugin.uploadSourceMaps.callCount).to.eq(1) expect(compilation.errors.length).to.eq(0) expect(compilation.warnings.length).to.eq(0) }) it('should add upload errors to compilation errors', async function () { - const compilation = { - errors: [], - warnings: [] - } - this.plugin.ignoreErrors = false + plugin.options.ignoreErrors = false - sinon.stub(this.plugin, 'uploadSourceMaps') + sinon.stub(plugin, 'uploadSourceMaps') .callsFake(() => { throw new Error() }) - await this.plugin.afterEmit(compilation) + await plugin.afterEmit(compilation) - expect(this.plugin.uploadSourceMaps.callCount).to.eq(1) + expect(plugin.uploadSourceMaps.callCount).to.eq(1) expect(compilation.warnings.length).to.eq(0) expect(compilation.errors.length).to.be.eq(1) expect(compilation.errors[0]).to.be.an.instanceof(Error) }) - it('should add validation errors to compilation', async function () { - const compilation = { - errors: [], - warnings: [], - getStats: () => ({ - toJson: () => ({ chunks: this.chunks }) - }) - } - - this.plugin = new HoneybadgerSourceMapPlugin({ - revision: 'fab5a8727c70647dcc539318b5b3e9b0cb8ae17b', - assetsUrl: 'https://cdn.example.com/assets' - }) - - sinon.stub(this.plugin, 'uploadSourceMaps') - .callsFake(() => {}) + it('should not send a deploy notification if there are compilation errors', async function () { + sinon.stub(plugin, 'uploadSourceMaps') + .callsFake(() => { throw new Error() }) + sinon.stub(plugin, 'sendDeployNotification') - await this.plugin.afterEmit(compilation) + await plugin.afterEmit(compilation) - expect(this.plugin.uploadSourceMaps.callCount).to.eq(0) - expect(compilation.errors.length).to.eq(1) + expect(plugin.uploadSourceMaps.callCount).to.eq(1) + expect(plugin.sendDeployNotification.callCount).to.eq(0) }) }) describe('getAssets', function () { - beforeEach(function () { - this.chunks = [ - { - id: 0, - names: ['app'], - files: ['app.81c1.js', 'app.81c1.js.map'] - } - ] - this.compilation = { - getStats: () => ({ - toJson: () => ({ chunks: this.chunks }) - }) + const chunks = [ + { + id: 0, + names: ['app'], + files: ['app.5190.js', 'app.5190.js.map'] + }, + { + id: 1, + names: ['foo'], + files: ['foo.5190.js'] + }, + ] + const outputPath = '/fake/output/path' + const compilation = { + getStats: () => ({ + toJson: () => ({ chunks }) + }), + compiler: { outputPath }, + getPath: () => outputPath, + } + + const expectedAssets = [ + { + sourcemapFilePath: '/fake/output/path/app.5190.js.map', + sourcemapFilename: 'app.5190.js.map', + jsFilePath: '/fake/output/path/app.5190.js', + jsFilename: 'app.5190.js', } - }) + ] it('should return an array of js, sourcemap tuples', function () { - const assets = this.plugin.getAssets(this.compilation) - expect(assets).to.deep.eq([ - { sourceFile: 'app.81c1.js', sourceMap: 'app.81c1.js.map' } - ]) + const assets = plugin.getAssets(compilation) + expect(assets).to.deep.eq(expectedAssets) }) it('should ignore chunks that do not have a sourcemap asset', function () { - this.chunks = [ - { - id: 0, - names: ['app'], - files: ['app.81c1.js', 'app.81c1.js.map'] - } - ] - const assets = this.plugin.getAssets(this.compilation) - expect(assets).to.deep.eq([ - { sourceFile: 'app.81c1.js', sourceMap: 'app.81c1.js.map' } - ]) + const assets = plugin.getAssets(compilation) + expect(assets).to.deep.eq(expectedAssets) }) it('should get the source map files from auxiliaryFiles in Webpack 5', function () { - this.chunks = [ + const w5chunks = [ { id: 0, - names: ['vendor'], - files: ['vendor.5190.js'], - auxiliaryFiles: ['vendor.5190.js.map'] + names: ['app'], + files: ['app.5190.js'], + auxiliaryFiles: ['app.5190.js.map'] } ] - const assets = this.plugin.getAssets(this.compilation) - expect(assets).to.deep.eq([ - { sourceFile: 'vendor.5190.js', sourceMap: 'vendor.5190.js.map' } - ]) - }) - }) - - describe('uploadSourceMaps', function () { - beforeEach(function () { - this.compilation = { name: 'test', errors: [] } - this.assets = [ - { sourceFile: 'vendor.5190.js', sourceMap: 'vendor.5190.js.map' }, - { sourceFile: 'app.81c1.js', sourceMap: 'app.81c1.js.map' } - ] - sinon.stub(this.plugin, 'getAssets').returns(this.assets) - sinon.stub(this.plugin, 'uploadSourceMap') - .callsFake(() => {}) - }) - - afterEach(function () { - sinon.restore() - }) - - it('should call uploadSourceMap for each chunk', async function () { - await this.plugin.uploadSourceMaps(this.compilation) - - expect(this.plugin.getAssets.callCount).to.eq(1) - expect(this.compilation.errors.length).to.eq(0) - expect(this.plugin.uploadSourceMap.callCount).to.eq(2) - - expect(this.plugin.uploadSourceMap.getCall(0).args[0]) - .to.deep.eq({ name: 'test', errors: [] }) - expect(this.plugin.uploadSourceMap.getCall(0).args[1]) - .to.deep.eq({ sourceFile: 'vendor.5190.js', sourceMap: 'vendor.5190.js.map' }) - - expect(this.plugin.uploadSourceMap.getCall(1).args[0]) - .to.deep.eq({ name: 'test', errors: [] }) - expect(this.plugin.uploadSourceMap.getCall(1).args[1]) - .to.deep.eq({ sourceFile: 'app.81c1.js', sourceMap: 'app.81c1.js.map' }) - }) - - it('should throw an error if the uploadSourceMap function returns an error', function () { - this.plugin.uploadSourceMap.restore() - - const error = new Error() - sinon.stub(this.plugin, 'uploadSourceMap') - .callsFake(() => {}) - .rejects(error) - - // Chai doesnt properly async / await rejections, so we gotta work around it - // with a...Promise ?! - this.plugin.uploadSourceMaps(this.compilation) - .catch((err) => expect(err).to.eq(error)) - }) - - context('If no sourcemaps are found', function () { - it('Should warn a user if silent is false', async function () { - this.plugin.getAssets.restore() - sinon.stub(this.plugin, 'getAssets').returns([]) - const info = sinon.stub(console, 'info') - - nock(TEST_ENDPOINT) - .filteringRequestBody(function (_body) { return '*' }) - .post(SOURCEMAP_PATH, '*') - .reply(200, JSON.stringify({ status: 'OK' })) - - const { compilation } = this - this.plugin.silent = false - - await this.plugin.uploadSourceMaps(compilation) - - expect(info.calledWith(this.plugin.noAssetsFoundMessage)).to.eq(true) - }) - - it('Should not warn a user if silent is true', async function () { - this.plugin.getAssets.restore() - sinon.stub(this.plugin, 'getAssets').returns([]) - const info = sinon.stub(console, 'info') - - nock(TEST_ENDPOINT) - .post(SOURCEMAP_PATH) - .reply(200, JSON.stringify({ status: 'OK' })) - - const { compilation } = this - this.plugin.silent = true - - await this.plugin.uploadSourceMaps(compilation) - - expect(info.notCalled).to.eq(true) - }) - }) - }) - - describe('uploadSourceMap', function () { - beforeEach(function () { - this.outputPath = '/fake/output/path' - this.info = sinon.stub(console, 'info') - this.compilation = { - assets: { - 'vendor.5190.js.map': { source: () => '{"version":3,"sources":[]' }, - 'app.81c1.js.map': { source: () => '{"version":3,"sources":[]' } - }, - compiler: { - outputPath: this.outputPath - }, - errors: [], - getPath: () => this.outputPath - } - - this.chunk = { - sourceFile: 'vendor.5190.js', - sourceMap: 'vendor.5190.js.map' - } - - this.spyReadFile = sinon - .stub(fs, 'readFile') - .callsFake(() => Promise.resolve('data')) - }) - - afterEach(function () { - sinon.restore() - }) - - it('should callback without err param if upload is success', async function () { - // FIXME/TODO test multipart form body ... it isn't really supported easily by nock - nock(TEST_ENDPOINT) - .post(SOURCEMAP_PATH) - .reply(201, JSON.stringify({ status: 'OK' })) - - const { compilation, chunk } = this - - await this.plugin.uploadSourceMap(compilation, chunk) - - expect(console.info.calledWith('Uploaded vendor.5190.js.map to Honeybadger API')).to.eq(true) - }) - - it('should not log upload to console if silent option is true', async function () { - nock(TEST_ENDPOINT) - .post(SOURCEMAP_PATH) - .reply(201, JSON.stringify({ status: 'OK' })) - - const { compilation, chunk } = this - this.plugin.silent = true - - await this.plugin.uploadSourceMap(compilation, chunk) - - expect(this.info.notCalled).to.eq(true) - }) - - it('should log upload to console if silent option is false', async function () { - nock(TEST_ENDPOINT) - .post(SOURCEMAP_PATH) - .reply(201, JSON.stringify({ status: 'OK' })) - - const { compilation, chunk } = this - this.plugin.silent = false - - await this.plugin.uploadSourceMap(compilation, chunk) - - expect(this.info.calledWith('Uploaded vendor.5190.js.map to Honeybadger API')).to.eq(true) - }) - - it('should return error message if failure response includes message', function () { - nock(TEST_ENDPOINT) - .post(SOURCEMAP_PATH) - .reply( - 422, - JSON.stringify({ error: 'The "source_map" parameter is required' }) - ) - - const { compilation, chunk } = this - - this.plugin.uploadSourceMap(compilation, chunk).catch((err) => { - expect(err).to.deep.include({ - message: 'failed to upload vendor.5190.js.map to Honeybadger API: The "source_map" parameter is required' - }) - }) - }) - - it('should handle error response with empty body', function () { - nock(TEST_ENDPOINT) - .post(SOURCEMAP_PATH) - .reply(422, null) - - const { compilation, chunk } = this - - this.plugin.uploadSourceMap(compilation, chunk).catch((err) => { - expect(err.message).to.match(/failed to upload vendor\.5190.js\.map to Honeybadger API: [\w\s]+/) - }) - }) - - it('should handle HTTP request error', function () { - nock(TEST_ENDPOINT) - .post(SOURCEMAP_PATH) - .replyWithError('something awful happened') - - const { compilation, chunk } = this - - this.plugin.uploadSourceMap(compilation, chunk).catch((err) => { - expect(err).to.deep.include({ - message: 'failed to upload vendor.5190.js.map to Honeybadger API: something awful happened' + const w5compilation = { + ...compilation, + getStats: () => ({ + toJson: () => ({ chunks: w5chunks }) }) - }) - }) - - it('should make a request to a configured endpoint', async function () { - const endpoint = 'https://my-special-endpoint' - const plugin = new HoneybadgerSourceMapPlugin({ ...this.options, endpoint: `${endpoint}${SOURCEMAP_PATH}` }) - nock(endpoint) - .post(SOURCEMAP_PATH) - .reply(201, JSON.stringify({ status: 'OK' })) - - const { compilation, chunk } = this - - await plugin.uploadSourceMap(compilation, chunk) - expect(this.info.calledWith('Uploaded vendor.5190.js.map to Honeybadger API')).to.eq(true) - }) - - it('should build correct assert url', function () { - const sourceFile1 = '/js/app.js' - const sourceFile2 = 'js/app.js' - - const plugin = new HoneybadgerSourceMapPlugin({ assetsUrl: 'https://example.com' }); - expect(plugin.assetsUrl).to.eq('https://example.com') - expect(plugin.getUrlToAsset(sourceFile1)).to.eq('https://example.com/js/app.js') - expect(plugin.getUrlToAsset(sourceFile2)).to.eq('https://example.com/js/app.js') - - plugin.assetsUrl = 'https://example.com/' - expect(plugin.assetsUrl).to.eq('https://example.com/') - expect(plugin.getUrlToAsset(sourceFile1)).to.eq('https://example.com/js/app.js') - expect(plugin.getUrlToAsset(sourceFile2)).to.eq('https://example.com/js/app.js') - }) - }) - - describe('sendDeployNotification', function () { - beforeEach(function () { - this.info = sinon.stub(console, 'info') - }) - - afterEach(function () { - sinon.restore() - }) - - it('should send a deploy notification if all keys are present', async function () { - const options = { - apiKey: 'abcd1234', - assetsUrl: 'https://cdn.example.com/assets', - deploy: { - environment: 'production', - repository: 'https://cdn.example.com', - localUsername: 'bugs' - } - } - const plugin = new HoneybadgerSourceMapPlugin({ ...options }) - nock(TEST_ENDPOINT) - .post(DEPLOY_PATH) - .reply(201, JSON.stringify({ status: 'OK' })) - - await plugin.sendDeployNotification() - expect(this.info.calledWith('Successfully sent deploy notification to Honeybadger API.')).to.eq(true) - }) - - it('should send a deploy notification with defaults if deploy is true', async function () { - const options = { - apiKey: 'abcd1234', - assetsUrl: 'https://cdn.example.com/assets', - deploy: true, - revision: 'o8y787g26574t4' } - const plugin = new HoneybadgerSourceMapPlugin({ ...options }) - - const scope = nock(TEST_ENDPOINT) - .post(DEPLOY_PATH, { deploy: { revision: options.revision } }) - .reply(201, JSON.stringify({ status: 'OK' })) - await plugin.sendDeployNotification() - expect(scope.isDone()).to.eq(true) + const assets = plugin.getAssets(w5compilation) + expect(assets).to.deep.eq(expectedAssets) }) }) }) diff --git a/packages/webpack/test/helpers.test.js b/packages/webpack/test/helpers.test.js index 2d05383a6..c0909d315 100644 --- a/packages/webpack/test/helpers.test.js +++ b/packages/webpack/test/helpers.test.js @@ -1,7 +1,6 @@ /* eslint-env mocha */ import chai from 'chai' -import { REQUIRED_FIELDS } from '../src/constants' import * as helpers from '../src/helpers' const expect = chai.expect @@ -58,59 +57,4 @@ describe('helpers', function () { expect(result.length).to.eq(0) }) }) - - describe('validateOptions', function () { - it('should return null if all required options are supplied', function () { - const options = { - apiKey: 'abcd1234', - revision: 'fab5a8727c70647dcc539318b5b3e9b0cb8ae17b', - assetsUrl: 'https://cdn.example.com/assets' - } - const result = helpers.validateOptions(options) - expect(result).to.eq(null) - }) - - it('should return an error if apiKey is not supplied', function () { - const options = { - revision: 'fab5a8727c70647dcc539318b5b3e9b0cb8ae17b', - assetsUrl: 'https://cdn.example.com/assets' - } - const result = helpers.validateOptions(options) - expect(result).to.be.an.instanceof(Array) - expect(result.length).to.eq(1) - expect(result[0]).to.be.an.instanceof(Error) - expect(result[0]).to.include({ message: 'required field, \'apiKey\', is missing.' }) - }) - - it('should return an error if assetsUrl is not supplied', function () { - const options = { - apiKey: 'abcd1234', - revision: 'fab5a8727c70647dcc539318b5b3e9b0cb8ae17b' - } - const result = helpers.validateOptions(options) - expect(result).to.be.an.instanceof(Array) - expect(result.length).to.eq(1) - expect(result[0]).to.be.an.instanceof(Error) - expect(result[0]).to.include({ message: 'required field, \'assetsUrl\', is missing.' }) - }) - - it('should handle multiple missing required options', function () { - const options = {} - const result = helpers.validateOptions(options) - expect(result).to.be.an.instanceof(Array) - expect(result.length).to.eq(REQUIRED_FIELDS.length) - }) - - it('should handle null for options', function () { - const result = helpers.validateOptions(null) - expect(result).to.be.an.instanceof(Array) - expect(result.length).to.eq(REQUIRED_FIELDS.length) - }) - - it('should handle no options passed', function () { - const result = helpers.validateOptions() - expect(result).to.be.an.instanceof(Array) - expect(result.length).to.eq(REQUIRED_FIELDS.length) - }) - }) }) diff --git a/packages/webpack/test/integration.test.js b/packages/webpack/test/integration.test.js index d5d99ea05..9a6ab1c46 100644 --- a/packages/webpack/test/integration.test.js +++ b/packages/webpack/test/integration.test.js @@ -3,88 +3,96 @@ // eslint-disable-next-line import/default import HoneybadgerSourceMapPlugin from '../src/HoneybadgerSourceMapPlugin' import webpack from 'webpack' -import chai from 'chai' +import { expect } from 'chai' import path from 'path' import nock from 'nock' import * as sinon from 'sinon' -const expect = chai.expect - -const consoleInfo = sinon.stub(console, 'info') const ASSETS_URL = 'https://cdn.example.com/assets' -const TEST_ENDPOINT = 'https://api.honeybadger.io' +const TEST_ENDPOINT = 'https://test.honeybadger.io' const SOURCEMAP_PATH = '/v1/source_maps' const DEPLOY_PATH = '/v1/deploys' -it('only uploads source maps if no deploy config', function (done) { - nock(TEST_ENDPOINT) - .post(SOURCEMAP_PATH) - .reply(201, JSON.stringify({ status: 'OK' })) - - const webpackConfig = { +describe('integration', () => { + let sourcemapNock + let deployNock + const baseWebpackConfig = { mode: 'development', entry: path.join(__dirname, 'fixtures/uncompiled.js'), output: { path: path.join(__dirname, '../tmp/') }, devtool: 'source-map', - plugins: [new HoneybadgerSourceMapPlugin({ - apiKey: 'abc123', - retries: 0, - assetsUrl: ASSETS_URL, - revision: 'master' - })] } - webpack(webpackConfig, (err, stats) => { - expect(err).to.eq(null) + const baseHbConfig = { + apiKey: 'abc123', + retries: 0, + assetsUrl: ASSETS_URL, + revision: 'master', + endpoint: `${TEST_ENDPOINT}${SOURCEMAP_PATH}`, + deployEndpoint: `${TEST_ENDPOINT}${DEPLOY_PATH}`, + } - const info = stats.toJson('errors-warnings') - expect(info.errors.length).to.equal(0) - expect(info.warnings.length).to.equal(0) + beforeEach(() => { + nock.disableNetConnect() + sourcemapNock = nock(TEST_ENDPOINT) + .post(SOURCEMAP_PATH) + .reply(201, JSON.stringify({ status: 'OK' })) + deployNock = nock(TEST_ENDPOINT) + .post(DEPLOY_PATH) + .reply(201, JSON.stringify({ status: 'OK' })) + }) - expect(consoleInfo.calledWith('Uploaded main.js.map to Honeybadger API')).to.eq(true) - expect(consoleInfo.calledWith('Successfully sent deploy notification to Honeybadger API.')).to.eq(false) - done() + afterEach(() => { + sinon.restore() + nock.cleanAll() }) -}) -it('uploads source maps and sends deployment notification if configured', function (done) { - nock(TEST_ENDPOINT) - .post(SOURCEMAP_PATH) - .reply(201, JSON.stringify({ status: 'OK' })) - nock(TEST_ENDPOINT) - .post(DEPLOY_PATH) - .reply(201, JSON.stringify({ status: 'OK' })) + it('uploads sourcemaps but does not send deploy notification if no deploy config', function (done) { + const webpackConfig = { + ...baseWebpackConfig, + plugins: [new HoneybadgerSourceMapPlugin(baseHbConfig)] + } - const webpackConfig = { - mode: 'development', - entry: path.join(__dirname, 'fixtures/uncompiled.js'), - output: { - path: path.join(__dirname, '../tmp/') - }, - devtool: 'source-map', - plugins: [new HoneybadgerSourceMapPlugin({ - apiKey: 'abc123', - retries: 0, - assetsUrl: ASSETS_URL, - revision: 'master', - deploy: { - environment: 'production', - repository: 'https://cdn.example.com', - localUsername: 'Jane' - } - })] - } + webpack(webpackConfig, (err, stats) => { + expect(err).to.eq(null) + + const info = stats.toJson('errors-warnings') + expect(info.errors.length).to.equal(0) + expect(info.warnings.length).to.equal(0) + + expect(sourcemapNock.isDone()).to.equal(true) + expect(deployNock.isDone()).to.equal(false) - webpack(webpackConfig, (err, stats) => { - expect(err).to.eq(null) - - const info = stats.toJson('errors-warnings') - expect(info.errors.length).to.equal(0) - expect(info.warnings.length).to.equal(0) + done() + }) + }) + + it('uploads source maps and sends deployment notification if configured', function (done) { + const webpackConfig = { + ...baseWebpackConfig, + plugins: [new HoneybadgerSourceMapPlugin({ + ...baseHbConfig, + deploy: { + environment: 'production', + repository: 'https://cdn.example.com', + localUsername: 'Jane' + }, + })] + } + + webpack(webpackConfig, (err, stats) => { + expect(err).to.eq(null) + + const info = stats.toJson('errors-warnings') + expect(info.errors.length).to.equal(0) + expect(info.warnings.length).to.equal(0) + + expect(sourcemapNock.isDone()).to.equal(true) + expect(deployNock.isDone()).to.equal(true) - expect(consoleInfo.calledWith('Uploaded main.js.map to Honeybadger API')).to.eq(true) - expect(consoleInfo.calledWith('Successfully sent deploy notification to Honeybadger API.')).to.eq(true) - done() + done() + }) }) }) + diff --git a/packages/webpack/test/resolvePromiseWithWorkers.test.js b/packages/webpack/test/resolvePromiseWithWorkers.test.js deleted file mode 100644 index 9f6046ae7..000000000 --- a/packages/webpack/test/resolvePromiseWithWorkers.test.js +++ /dev/null @@ -1,103 +0,0 @@ -/* eslint-env mocha */ - -import chai from 'chai' -import * as sinon from 'sinon' - -import { resolvePromiseWithWorkers } from '../src/resolvePromiseWithWorkers' - -const expect = chai.expect - -function asyncPromiseGenerator ( - name, - timeout, - traceCallback -) { - return new Promise((resolve) => { - if (typeof traceCallback === 'function') { - traceCallback(`start ${name}`) - } - - setTimeout(() => { - if (typeof traceCallback === 'function') { - traceCallback(`resolve ${name}`) - } - resolve(name) - }, timeout) - }) -} - -function assertCallSequence ( - spy, - sequence -) { - for (let i = 0; i < sequence.length; ++i) { - sinon.assert.calledWith(spy.getCall(i), sequence[i]) - } -} - -const promises = [ - () => asyncPromiseGenerator('First', 1), - () => asyncPromiseGenerator('Second', 10), - () => asyncPromiseGenerator('Third', 3) -] - -describe('resolvePromiseWithWorkers', function () { - it('should resolve all promises', async function () { - expect(await resolvePromiseWithWorkers(promises, 5)) - .to.deep.eq(['First', 'Second', 'Third']) - }) - - it('should resolve all promises if the number of workers is lower than the number of promises', async function () { - expect(await resolvePromiseWithWorkers(promises, 1)) - .to.deep.eq(['First', 'Second', 'Third']) - }) - - it('should resolve the promises sequentially if the number of worker is 1', async function () { - const spy = sinon.spy() - const promisesWithCallback = [ - () => asyncPromiseGenerator('First', 1, spy), - () => asyncPromiseGenerator('Second', 3, spy), - () => asyncPromiseGenerator('Third', 5, spy) - ] - - await resolvePromiseWithWorkers(promisesWithCallback, 1) - - const sequence = [ - 'start First', - 'resolve First', - 'start Second', - 'resolve Second', - 'start Third', - 'resolve Third' - ] - assertCallSequence(spy, sequence) - }) - - it('should resolve the promises by workers', async function () { - const spy = sinon.spy() - const promisesWithCallback = [ - () => asyncPromiseGenerator('First', 1, spy), - () => - asyncPromiseGenerator( - 'Second', - 50 /* A very long promise that should keep a worker busy */, - spy - ), - () => asyncPromiseGenerator('Third', 1, spy), - () => asyncPromiseGenerator('Fourth', 1, spy) - ] - - await resolvePromiseWithWorkers(promisesWithCallback, 2) - const sequence = [ - 'start First', - 'start Second', - 'resolve First', - 'start Third', - 'resolve Third', - 'start Fourth', - 'resolve Fourth', - 'resolve Second' - ] - assertCallSequence(spy, sequence) - }) -})