Skip to content

Adding support for retry with backoff #502

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
38 changes: 36 additions & 2 deletions src/RollbarSourceMapPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -26,6 +31,7 @@ class RollbarSourceMapPlugin {
this.ignoreErrors = ignoreErrors;
this.rollbarEndpoint = rollbarEndpoint;
this.encodeFilename = encodeFilename;
this.maxRetries = maxRetries;
}

async afterEmit(compilation) {
Expand Down Expand Up @@ -164,9 +170,37 @@ 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 {
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) {
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;
78 changes: 78 additions & 0 deletions test/RollbarSourceMapPlugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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...');
});
});
});