Madogiwa Blog

主に技術系の学習メモに使っていきます。

Viteでビルド時にRollbarにSourcemapをアップロードするプラグインを自作したのでメモ📝

Viteを使っていてRollbarにSourceMapをアップロードする際には下記の通りですが、

Vite plugin There is a community-maintained Rollbar Sourcemap Plugin for Vite. Please check the Readme doc for details on the project and usage instructions. https://docs.rollbar.com/docs/source-maps#vite-plugin

以下のコミュニティメンテのPluginの利用が推奨されてますが、Vite v5に対応してなさそうだったので参考にして自作してみたのでメモ📝

github.com

以下がそのプラグインです。(基本はコミュニティメンテのPluginのロジックを踏襲しつつTypeScriptで書き直しつつ、メソッドを整理したりエラーハンドリング周りを簡素化しただけ)

/* eslint-disable no-console */
import type { Plugin } from "vite";
import { existsSync, readFileSync } from "fs";
import { resolve } from "path";
import { glob } from "glob";
const ROLLBAR_ENDPOINT = "https://api.rollbar.com/api/1/sourcemap";

type rollbarSourcemapsOptions = {
  accessToken: string;
  version: string;
  baseUrl: string;
  silent?: boolean;
  ignoreUploadErrors?: boolean;
  base?: string;
  outputDir?: string;
};
export default function rollbarSourcemaps({
  accessToken,
  version,
  baseUrl,
  silent = false,
  ignoreUploadErrors = true,
  base = "/",
  outputDir = "dist",
}: rollbarSourcemapsOptions): Plugin {
  return {
    name: "vite-plugin-rollbar",
    async writeBundle() {
      const files = await glob("./**/*.map", { cwd: outputDir });
      const sourcemaps: RollbarSourceMap[] = files
        .map((file) => {
          const sourcePath = calcSourcePath({ sourcemap: file, outputDir });
          if (sourcePath === null) {
            console.error(`No corresponding source found for '${file}'`, true);
            return null;
          }
          const sourcemapLocation = resolve(outputDir, file);
          const sourcemap = buildRollbarSourcemap({ base, sourcePath, sourcemapLocation });
          if (sourcemap === null) console.error(`Error reading sourcemap file ${sourcemapLocation}`, true);
          return sourcemap;
        })
        .filter((sourcemap) => sourcemap !== null);

      if (!sourcemaps.length) return;

      try {
        await Promise.all(
          sourcemaps.map((asset) => {
            const form = buildPostFormData({ accessToken, version, baseUrl, asset });
            return uploadSourcemap(form, { filename: asset.original_file, silent });
          }),
        );
      } catch (error) {
        if (ignoreUploadErrors) {
          console.error("Uploading sourcemaps to Rollbar failed: ", error);
          return;
        }
        throw error;
      }
    },
  };
}

async function postRollbarSourcemap(body: FormData): Promise<Response> {
  const res = await fetch(ROLLBAR_ENDPOINT, { method: "POST", body });
  if (!res.ok) throw new Error(`Failed to pots sourcemap to Rollbar: ${res.statusText}`);
  return res;
}

async function uploadSourcemap(form: FormData, { filename, silent }: { filename: string; silent: boolean }) {
  let res;
  try {
    res = await postRollbarSourcemap(form);
  } catch (err: unknown) {
    const error = err as Error;
    const errMessage = `Failed to upload ${filename} to Rollbar: ${error.message}`;
    throw new Error(errMessage);
  }

  if (res.ok || !silent) console.info(`Uploaded ${filename} to Rollbar`);
}

type RollbarSourceMap = {
  content: string;
  sourcemap_url: string;
  original_file: string;
};
function buildRollbarSourcemap({
  base,
  sourcePath,
  sourcemapLocation,
}: {
  base: string;
  sourcePath: string;
  sourcemapLocation: string;
}): RollbarSourceMap | null {
  try {
    return {
      content: readFileSync(sourcemapLocation, "utf8"),
      sourcemap_url: sourcemapLocation,
      original_file: `${base}${sourcePath}`,
    };
  } catch (_error) {
    return null;
  }
}

function buildPostFormData({
  accessToken,
  version,
  baseUrl,
  asset,
}: {
  accessToken: string;
  version: string;
  baseUrl: string;
  asset: RollbarSourceMap;
}) {
  const form = new FormData();

  form.set("access_token", accessToken);
  form.set("version", version);
  form.set("minified_url", `${baseUrl}${asset.original_file}`);
  form.set("source_map", new Blob([asset.content]), asset.original_file);
  return form;
}

function calcSourcePath({ sourcemap, outputDir }: { sourcemap: string; outputDir: string }): string | null {
  const sourcePath = sourcemap.replace(/\.map$/, "");
  const sourceFilename = resolve(outputDir, sourcePath);
  if (!existsSync(sourceFilename)) return null;
  return sourcePath;
}

以下のような感じで利用することができます。

const setupRollbarPlugin = () => {
  const rollbarConfig = {
    accessToken: process.env.ROLLBAR_POST_SERVER_ITEM_ACCESS_TOKEN || "",
    version: process.env.SOURCE_VERSION || "unknown",
    baseUrl: APP_ASSETS_HOST_URL,
    ignoreUploadErrors: true,
    outputDir: OUTPUT_DIR,
    silent: false,
  };
  return viteRollbar(rollbarConfig);
};