From 9ab6bd2300f8ef0a5398e7e9d401403a4f82149c Mon Sep 17 00:00:00 2001 From: Jonathan Felchlin Date: Fri, 24 Jun 2022 11:24:16 -0700 Subject: [PATCH 1/2] Adding support for retry with backoff --- README.md | 4 ++ src/RollbarSourceMapPlugin.js | 36 ++++++++++++- test/RollbarSourceMapPlugin.test.js | 78 +++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fd47b6cb..5b6e5331 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,10 @@ A string defining the Rollbar API endpoint to upload the sourcemaps to. It can b Set to true to encode the filename. NextJS will reference the encode the URL when referencing the minified script which must match exactly with the minified file URL uploaded to Rollbar. +### `maxRetries: string` **(default: `0`)** + +The maximum number of times to retry if uploading fails. Retries are implemented with exponential backoff to avoid inundating a potentially overloaded server. + ## Webpack Sourcemap Configuration The [`output.devtool`](https://webpack.js.org/configuration/devtool/) field in webpack configuration controls how sourcemaps are generated. diff --git a/src/RollbarSourceMapPlugin.js b/src/RollbarSourceMapPlugin.js index 33efaf8f..aa6fc77e 100644 --- a/src/RollbarSourceMapPlugin.js +++ b/src/RollbarSourceMapPlugin.js @@ -7,6 +7,10 @@ import VError from 'verror'; import { handleError, validateOptions } from './helpers'; import { PLUGIN_NAME, ROLLBAR_ENDPOINT } from './constants'; +function wait(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + class RollbarSourceMapPlugin { constructor({ accessToken, @@ -16,7 +20,8 @@ class RollbarSourceMapPlugin { silent = false, ignoreErrors = false, rollbarEndpoint = ROLLBAR_ENDPOINT, - encodeFilename = false + encodeFilename = false, + maxRetries = 0 }) { this.accessToken = accessToken; this.version = version; @@ -26,6 +31,7 @@ class RollbarSourceMapPlugin { this.ignoreErrors = ignoreErrors; this.rollbarEndpoint = rollbarEndpoint; this.encodeFilename = encodeFilename; + this.maxRetries = maxRetries; } async afterEmit(compilation) { @@ -164,9 +170,35 @@ class RollbarSourceMapPlugin { process.stdout.write('\n'); } return Promise.all( - assets.map(asset => this.uploadSourceMap(compilation, asset)) + assets.map(asset => this.uploadSourceMapWithRetry(compilation, asset)) ); } + + async uploadSourceMapWithRetry(compilation, asset, depth = 0) { + try { + // eslint-disable-next-line no-console + console.info(`Uploading ${asset.sourceMap} to Rollbar`); + return await this.uploadSourceMap(compilation, asset); + } catch (error) { + if (depth >= this.maxRetries) { + throw error; + } + // Delay with exponential backoff + const delay = 2 ** depth * 10; + if (!this.silent) { + // eslint-disable-next-line no-console + console.info( + `Uploading ${asset.sourceMap} to Rollbar failed with error: ${ + error.message || error + }` + ); + // eslint-disable-next-line no-console + console.info(`Retrying in ${delay}ms...`); + } + await wait(delay); + return this.uploadSourceMapWithRetry(compilation, asset, depth + 1); + } + } } module.exports = RollbarSourceMapPlugin; diff --git a/test/RollbarSourceMapPlugin.test.js b/test/RollbarSourceMapPlugin.test.js index 288619af..ea890b22 100644 --- a/test/RollbarSourceMapPlugin.test.js +++ b/test/RollbarSourceMapPlugin.test.js @@ -551,4 +551,82 @@ describe('RollbarSourceMapPlugin', () => { ); }); }); + + describe('uploadSourceMapWithRetry', () => { + let compilation; + let chunk; + let info; + const err = new Error('502 - Bad Gateway'); + let uploadSourceMapSpy; + + beforeEach(() => { + info = jest.spyOn(console, 'info').mockImplementation(); + compilation = {}; + + chunk = { + sourceFile: 'vendor.5190.js', + sourceMap: 'vendor.5190.js.map' + }; + }); + + it('calls uploadSourceMap once if no errors are thrown', async () => { + uploadSourceMapSpy = jest + .spyOn(plugin, 'uploadSourceMap') + .mockImplementation(); + + await plugin.uploadSourceMapWithRetry(compilation, chunk); + expect(uploadSourceMapSpy).toHaveBeenCalledTimes(1); + expect(uploadSourceMapSpy).toHaveBeenCalledWith(compilation, chunk); + expect(info).toHaveBeenCalledWith( + 'Uploading vendor.5190.js.map to Rollbar' + ); + }); + + it('throws when uploadSourceMap fails, if maxRetries is not set', async () => { + uploadSourceMapSpy = jest + .spyOn(plugin, 'uploadSourceMap') + .mockImplementationOnce(() => Promise.reject(err)); + + await expect( + plugin.uploadSourceMapWithRetry(compilation, chunk) + ).rejects.toThrow(err); + expect(uploadSourceMapSpy).toHaveBeenCalledWith(compilation, chunk); + }); + + it('retries when uploadSourceMap fails', async () => { + plugin.maxRetries = 1; + uploadSourceMapSpy = jest + .spyOn(plugin, 'uploadSourceMap') + .mockImplementation(() => Promise.reject(err)); + + await expect( + plugin.uploadSourceMapWithRetry(compilation, chunk) + ).rejects.toThrow(err); + expect(uploadSourceMapSpy).toHaveBeenCalledTimes(2); + expect(uploadSourceMapSpy).toHaveBeenNthCalledWith(2, compilation, chunk); + expect(info).toHaveBeenCalledWith( + 'Uploading vendor.5190.js.map to Rollbar failed with error: 502 - Bad Gateway' + ); + }); + + it('succeeds with exponential backoff when uploadSourceMap succeeds on a subsequent try', async () => { + plugin.maxRetries = 5; + uploadSourceMapSpy = jest + .spyOn(plugin, 'uploadSourceMap') + .mockImplementationOnce(() => Promise.reject(err)) + .mockImplementationOnce(() => Promise.reject(err)) + .mockImplementationOnce(() => Promise.reject(err)) + .mockImplementationOnce(() => Promise.reject(err)) + .mockImplementationOnce(() => Promise.reject(err)) + .mockImplementationOnce(() => undefined); + + await plugin.uploadSourceMapWithRetry(compilation, chunk); + expect(uploadSourceMapSpy).toHaveBeenCalledTimes(6); + expect(info).toHaveBeenCalledWith('Retrying in 10ms...'); + expect(info).toHaveBeenCalledWith('Retrying in 20ms...'); + expect(info).toHaveBeenCalledWith('Retrying in 40ms...'); + expect(info).toHaveBeenCalledWith('Retrying in 80ms...'); + expect(info).toHaveBeenCalledWith('Retrying in 160ms...'); + }); + }); }); From 630864745598f465f2f5e627c3a77cadf60486e0 Mon Sep 17 00:00:00 2001 From: Jonathan Felchlin Date: Thu, 30 Jun 2022 11:17:48 -0700 Subject: [PATCH 2/2] Ensuring silent mode is enforced --- src/RollbarSourceMapPlugin.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/RollbarSourceMapPlugin.js b/src/RollbarSourceMapPlugin.js index aa6fc77e..9f9fd0d4 100644 --- a/src/RollbarSourceMapPlugin.js +++ b/src/RollbarSourceMapPlugin.js @@ -176,8 +176,10 @@ class RollbarSourceMapPlugin { async uploadSourceMapWithRetry(compilation, asset, depth = 0) { try { - // eslint-disable-next-line no-console - console.info(`Uploading ${asset.sourceMap} to Rollbar`); + if (!this.silent) { + // eslint-disable-next-line no-console + console.info(`Uploading ${asset.sourceMap} to Rollbar`); + } return await this.uploadSourceMap(compilation, asset); } catch (error) { if (depth >= this.maxRetries) {