背景
当前项目使用的vite作为打包工具,因此就使用了vite-plugin-sentry插件上传sourcemap。且在本地开发时可以上传sourcemap到sentry
问题
vite-plugin-sentry插件内部使用了sentry-cli,且在本地开发环境运行一切正常,但是接入github ci后就出现了下面截图中的问题
解决方式
- 网上的多种方法都试了一遍,还是没有解决
- 最后查看了webpack的webpack-sentry-plugin,这个插件内部并没有使用sentry-cli,所以最后采用webpack-sentry-plugin内部的实现方式上传
- 编写upload-sentry.js,如下
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable no-undef */
const fs = require('fs')
const path = require('path')
const child_process = require('child_process')
const request = require('request')
const PromisePool = require('es6-promise-pool')
const BASE_SENTRY_URL = 'https://sentry.io/api/0'
const DEFAULT_INCLUDE = /\.js$|\.map$/
const DEFAULT_TRANSFORM = (filename) => `~/${filename}`
const DEFAULT_DELETE_REGEX = /\.map$/
const DEFAULT_BODY_TRANSFORM = (version, projects) => ({ version, projects })
const DEFAULT_UPLOAD_FILES_CONCURRENCY = Infinity
const OUT_DIR_PATH = path.join(__dirname, '../dist/assets/')
class SentryPlugin {
constructor(options) {
// The baseSentryURL option was previously documented to have
// `/projects` on the end. We now expect the basic API endpoint
// but remove any `/projects` suffix for backwards compatibility.
const projectsRegex = /\/projects$/
if (options.baseSentryURL) {
if (projectsRegex.test(options.baseSentryURL)) {
// eslint-disable-next-line no-console
this.baseSentryURL = options.baseSentryURL.replace(projectsRegex, '')
} else {
this.baseSentryURL = options.baseSentryURL
}
} else {
this.baseSentryURL = BASE_SENTRY_URL
}
this.organizationSlug = options.organization || options.organisation
this.projectSlug = options.project
if (typeof this.projectSlug === 'string') {
this.projectSlug = [this.projectSlug]
}
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_TRANSFORM
this.suppressErrors = options.suppressErrors
this.suppressConflictError = options.suppressConflictError
this.createReleaseRequestOptions =
options.createReleaseRequestOptions || options.requestOptions || {}
if (typeof this.createReleaseRequestOptions === 'object') {
const { createReleaseRequestOptions } = this
this.createReleaseRequestOptions = () => createReleaseRequestOptions
}
this.uploadFileRequestOptions =
options.uploadFileRequestOptions || options.requestOptions || {}
if (typeof this.uploadFileRequestOptions === 'object') {
const { uploadFileRequestOptions } = this
this.uploadFileRequestOptions = () => uploadFileRequestOptions
}
if (options.requestOptions) {
// eslint-disable-next-line no-console
console.warn(
'requestOptions is deprecated. ' +
'use createReleaseRequestOptions and ' +
'uploadFileRequestOptions instead; ',
)
}
this.deleteAfterCompile = options.deleteAfterCompile
this.deleteRegex = options.deleteRegex || DEFAULT_DELETE_REGEX
this.uploadFilesConcurrency =
options.uploadFilesConcurrency || DEFAULT_UPLOAD_FILES_CONCURRENCY
this.apply()
}
async apply() {
const errors = this.ensureRequiredOptions()
if (errors) {
this.handleErrors(errors)
return
}
if (typeof this.releaseBody === 'function') {
this.releaseBody = this.releaseBody(this.releaseVersion, this.projectSlug)
}
const files = this.getDirFiles()
try {
await this.createRelease()
const poolPromise = this.uploadFiles(files)
poolPromise
.then(
async () => {
console.log('all file uploaded ')
},
async function (error) {
console.log('some file unload failed: ' + error.message)
},
)
.finally(async () => {
await this.deleteFiles()
})
} catch (error) {
this.handleErrors(error)
}
}
getDirFiles() {
var filesList = fs.readdirSync(OUT_DIR_PATH)
const files = this.getFiles(filesList)
return files
}
handleErrors(err) {
const errorMsg = `upload file to sentry error: ${err}`
if (
this.suppressErrors ||
(this.suppressConflictError && err.statusCode === 409)
) {
console.warn(errorMsg)
} else {
console.warn(errorMsg)
}
}
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
}
}
// eslint-disable-next-line class-methods-use-this
getAssetPath(name) {
return path.join(OUT_DIR_PATH, name.split('?')[0])
}
getFiles(filesList) {
return filesList
.map((name) => {
if (this.isIncludeOrExclude(name)) {
return { name, filePath: this.getAssetPath(name) }
}
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
}
// eslint-disable-next-line class-methods-use-this
combineRequestOptions(req, requestOptionsFunc) {
const requestOptions = requestOptionsFunc(req)
const combined = Object.assign({}, requestOptions, req)
if (requestOptions.headers) {
Object.assign(combined.headers, requestOptions.headers, req.headers)
}
if (requestOptions.auth) {
Object.assign(combined.auth, requestOptions.auth, req.auth)
}
return combined
}
async createRelease() {
await request(
this.combineRequestOptions(
{
url: `${this.sentryReleaseUrl()}/`,
method: 'POST',
auth: {
bearer: this.apiKey,
},
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(this.releaseBody),
},
this.createReleaseRequestOptions,
),
)
}
uploadFiles(files) {
const pool = new PromisePool(() => {
const file = files.pop()
if (!file) {
return null
}
return this.uploadFile(file)
}, this.uploadFilesConcurrency)
return pool.start()
}
async uploadFile({ filePath, name }) {
await request(
this.combineRequestOptions(
{
url: `${this.sentryReleaseUrl()}/${this.releaseVersion}/files/`,
method: 'POST',
auth: {
bearer: this.apiKey,
},
headers: {},
formData: {
file: fs.createReadStream(filePath),
name: this.filenameTransform(name),
},
},
this.uploadFileRequestOptions,
),
)
}
sentryReleaseUrl() {
return `${this.baseSentryURL}/organizations/${this.organizationSlug}/releases`
}
async deleteFiles() {
if (!this.deleteAfterCompile) return
setImmediate(() => {
const files = this.getDirFiles()
files
.filter(({ name }) => this.deleteRegex.test(name))
.map(({ filePath }) => {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath)
} else {
// eslint-disable-next-line no-console
console.warn(
`unable to delete '${filePath}'. ` +
'File does not exist; it may not have been created ' +
'due to a build error.',
)
}
})
})
}
}
const commit = child_process
.execSync('git show -s --format=%H')
.toString()
.trim()
const sentryCfg = {
organization: 'XXXXXX',
project: 'XXXXX',
apiKey: 'XXXX',
baseSentryURL: 'https://sentry.XXXXX/api/0',
}
new SentryPlugin({
release: commit,
apiKey: sentryCfg.apiKey,
deleteAfterCompile: true,
project: sentryCfg.project,
include: /(\.js|\.js\.map)$/,
organization: sentryCfg.organization,
baseSentryURL: sentryCfg.baseSentryURL,
filenameTransform: function (filename) {
return '~/' + filename
},
})
- npm scripts命令添加
"build": "vite build --mode prod && node ./deploy/upload-sentry.js",