Created
November 24, 2017 10:53
-
-
Save pladaria/296529fdf0df6aa87a45ca396882c311 to your computer and use it in GitHub Desktop.
Sentry artifacts upload plugin for webpack
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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