sourcemap上传的sentry

509 阅读1分钟

背景

当前项目使用的vite作为打包工具,因此就使用了vite-plugin-sentry插件上传sourcemap。且在本地开发时可以上传sourcemap到sentry

问题

vite-plugin-sentry插件内部使用了sentry-cli,且在本地开发环境运行一切正常,但是接入github ci后就出现了下面截图中的问题

image.png

解决方式

  • 网上的多种方法都试了一遍,还是没有解决
  • 最后查看了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",