Skip to content

Instantly share code, notes, and snippets.

@pladaria
Created November 24, 2017 10:53
Show Gist options
  • Save pladaria/296529fdf0df6aa87a45ca396882c311 to your computer and use it in GitHub Desktop.
Save pladaria/296529fdf0df6aa87a45ca396882c311 to your computer and use it in GitHub Desktop.
Sentry artifacts upload plugin for webpack
/*
* Original file:
* https://github.com/40thieves/webpack-sentry-plugin/blob/master/src/index.js
*/
const request = require('request-promise');
const fs = require('fs');
const crypto = require('crypto');
const {green, yellow, red} = require('colors/safe');
const Queue = require('promise-queue');
const DEFAULT_BASE_SENTRY_URL = 'https://sentry.io/api/0/projects';
const DEFAULT_INCLUDE = /\.js$|\.map$/;
const DEFAULT_FILENAME_TRANSFORM = filename => `~/${filename}`;
const DEFAULT_DELETE_REGEX = /\.map$/;
const DEFAULT_BODY_TRANSFORM = version => ({version});
const UPLOAD_CONCURRENCY = 5;
module.exports = class SentryPlugin {
constructor(options) {
this.baseSentryURL = options.baseSentryURL || DEFAULT_BASE_SENTRY_URL;
this.organizationSlug = options.organization || options.organisation;
this.projectSlug = options.project;
this.apiKey = options.apiKey;
this.releaseBody = options.releaseBody || DEFAULT_BODY_TRANSFORM;
this.releaseVersion = options.release;
this.include = options.include || DEFAULT_INCLUDE;
this.exclude = options.exclude;
this.filenameTransform = options.filenameTransform || DEFAULT_FILENAME_TRANSFORM;
this.suppressErrors = options.suppressErrors;
this.suppressConflictError = options.suppressConflictError;
this.deleteAfterCompile = options.deleteAfterCompile;
this.deleteRegex = options.deleteRegex || DEFAULT_DELETE_REGEX;
this.queue = new Queue(options.uploadConcurrency || UPLOAD_CONCURRENCY);
}
apply(compiler) {
compiler.plugin('after-emit', (compilation, cb) => {
const errors = this.ensureRequiredOptions();
if (errors) {
return this.handleErrors(errors, compilation, cb);
}
const files = this.getFiles(compilation);
if (typeof this.releaseVersion === 'function') {
this.releaseVersion = this.releaseVersion(compilation.hash);
}
if (typeof this.releaseBody === 'function') {
this.releaseBody = this.releaseBody(this.releaseVersion);
}
return this.createRelease()
.then(() => this.uploadFiles(files))
.then(() => cb())
.catch(err => this.handleErrors(err, compilation, cb));
});
compiler.plugin('done', stats => {
if (this.deleteAfterCompile) {
this.deleteFiles(stats);
}
});
}
handleErrors(err, compilation, cb) {
const errorMsg = `Sentry Plugin: ${err}`;
if (this.suppressErrors || (this.suppressConflictError && err.statusCode === 409)) {
compilation.warnings.push(errorMsg);
} else {
compilation.errors.push(errorMsg);
}
cb();
}
ensureRequiredOptions() {
if (!this.organizationSlug) {
return new Error('Must provide organization');
} else if (!this.projectSlug) {
return new Error('Must provide project');
} else if (!this.apiKey) {
return new Error('Must provide api key');
} else if (!this.releaseVersion) {
return new Error('Must provide release version');
} else {
return null;
}
}
getFiles(compilation) {
return Object.keys(compilation.assets)
.map(name => {
if (this.isIncludeOrExclude(name)) {
return {name, path: compilation.assets[name].existsAt};
}
return null;
})
.filter(i => i);
}
isIncludeOrExclude(filename) {
const isIncluded = this.include ? this.include.test(filename) : true;
const isExcluded = this.exclude ? this.exclude.test(filename) : false;
return isIncluded && !isExcluded;
}
createRelease() {
return request({
url: `${this.sentryReleaseUrl()}/`,
method: 'POST',
auth: {
bearer: this.apiKey,
},
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
body: JSON.stringify(this.releaseBody),
});
}
uploadFiles(files) {
Promise.all(files.map(this.uploadFile.bind(this)));
}
uploadFile({path, name}) {
const filename = this.filenameTransform(name);
return this.queue
.add(() =>
request({
url: `${this.sentryReleaseUrl()}/${this.releaseVersion}/files/`,
method: 'POST',
auth: {
bearer: this.apiKey,
},
formData: {
name: filename,
file: {
value: fs.createReadStream(path),
options: {
filename,
contentType: 'application/octet-stream',
},
},
},
})
)
.then(json => {
const res = JSON.parse(json);
const buffer = fs.readFileSync(path);
const sha1 = crypto
.createHash('sha1')
.update(buffer)
.digest('hex');
if (res.size !== buffer.length) {
throw Error(
`Sentry upload. Bad size. File: ${name} - reported: ${res.size} - real: ${buffer.byteLength}`
);
}
if (res.sha1 !== sha1) {
throw Error(
`Sentry upload. Bad hash. File: ${name} - reported: ${res.sha1} - real: ${sha1}`
);
}
console.log(`${green.bold('Sentry - upload OK')}: ${filename}`);
return res;
})
.catch(err => {
if (err.statusCode === 409) {
console.log(`${yellow.bold('Sentry - file exists')}: ${filename}`);
} else {
console.log(`${red.bold('Sentry - upload ERROR')} [${err.statusCode}]: ${filename}`);
}
});
}
sentryReleaseUrl() {
return `${this.baseSentryURL}/${this.organizationSlug}/${this.projectSlug}/releases`;
}
deleteFiles(stats) {
Object.keys(stats.compilation.assets)
.filter(name => this.deleteRegex.test(name))
.forEach(name => {
const {existsAt} = stats.compilation.assets[name];
fs.unlinkSync(existsAt);
});
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment