frontend/test/browser-symbols.test.ts
Rama master · réplica de lectura del repositorio original.
// @vitest-environment node
import { execFileSync } from 'node:child_process';
import { mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { resolve } from 'node:path';
import { SentryCli } from '@sentry/cli';
import { build } from 'vite';
import { afterEach, expect, it } from 'vitest';
import { browserSymbols } from '../scripts/sentry-build';
import { prepareBrowserUpload } from '../scripts/upload-sourcemaps';
const fixtures: string[] = [];
afterEach(() => {
for (const fixture of fixtures.splice(0)) {
if (!fixture.startsWith(realpathSync(tmpdir()))) throw new Error('临时目录必须属于系统临时目录');
rmSync(fixture, { recursive: true, force: true });
}
});
it('官方构建生成可复现 Debug ID,私有 map 可还原源码位置,公开目录只含正式文件与本次的 chunk', async () => {
const root = realpathSync(mkdtempSync(resolve(tmpdir(), 'peach-symbols-')));
fixtures.push(root);
const directory = resolve(root, 'artifacts');
const shipped = resolve(root, 'public');
writeFileSync(resolve(root, 'fixture.ts'), 'import "./fixture.css";\nexport function fail() { throw new TypeError("symbolication fixture"); }\n'
+ 'export const later = () => import("./later.ts");\n');
writeFileSync(resolve(root, 'later.ts'), 'export const value = "later chunk";\n');
writeFileSync(resolve(root, 'fixture.css'), '.fixture { color: red; }\n');
const compile = async (name: 'peach-app' | 'peach-pages') => {
await build({
// 公开目录就叫 `public`,不关掉 publicDir 的话 Vite 会把它整个抄回私有目录。
configFile: false, root, logLevel: 'silent', publicDir: false,
plugins: browserSymbols(name, shipped),
build: {
outDir: directory, emptyOutDir: false, sourcemap: 'hidden', minify: 'oxc',
lib: { entry: resolve(root, 'fixture.ts'), formats: ['es'], fileName: () => `${name}.js`, cssFileName: name },
rollupOptions: { output: {
assetFileNames: `${name}.[ext]`, chunkFileNames: `${name}.[name]-[hash].js`,
...(name === 'peach-pages' ? { codeSplitting: false } : {}),
} },
},
});
};
await compile('peach-app');
const first = readFileSync(resolve(shipped, 'peach-app.js'), 'utf8');
const chunk = readdirSync(shipped).find(name => /^peach-app\.later-[\w-]+\.js$/.test(name));
expect(chunk).toBeDefined();
expect(first).toContain(`import("./${chunk}")`);
writeFileSync(resolve(shipped, 'peach-app.stale-Old00000.js'), 'previous build');
await compile('peach-app');
expect(readFileSync(resolve(shipped, 'peach-app.js'), 'utf8')).toBe(first);
await compile('peach-pages');
expect(readdirSync(shipped).sort()).toEqual(['peach-app.css', 'peach-app.js', chunk, 'peach-pages.css', 'peach-pages.js']);
expect(first).not.toContain('sourceMappingURL');
const mapPath = resolve(directory, 'peach-app.js.map');
const map = JSON.parse(readFileSync(mapPath, 'utf8'));
expect(map.sourceRoot).toBeUndefined();
expect(map.sources.every((source: string) => !/^(?:[a-z]:|\/)/i.test(source))).toBe(true);
expect(map.debug_id).toMatch(/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/);
expect(first).toContain(`//# debugId=${map.debug_id}`);
const token = first.indexOf('TypeError("symbolication fixture")');
expect(token).toBeGreaterThan(0);
const before = first.slice(0, token).split('\n');
const output = execFileSync(SentryCli.getPath(), [
'sourcemaps', 'resolve', mapPath, '--line', String(before.length), '--column', String(before.at(-1)!.length),
], { encoding: 'utf8' });
expect(output).toContain('fixture.ts');
expect(output).toContain('symbolication fixture');
const env = { SENTRY_AUTH_TOKEN: 'test-only', SENTRY_ORG: 'fixture', SENTRY_PROJECT: 'fixture' };
expect(prepareBrowserUpload(env, directory, shipped)).toEqual([
'sourcemaps', 'upload', '--validate', '--strict', '--wait', '--url-prefix', 'app:///', directory,
]);
writeFileSync(resolve(shipped, 'peach-app.js'), 'mismatched bundle');
expect(() => prepareBrowserUpload(env, directory, shipped)).toThrow('分发产物不一致');
writeFileSync(resolve(shipped, 'peach-app.js'), first);
writeFileSync(mapPath, JSON.stringify({ ...map, debug_id: 'invalid' }));
expect(() => prepareBrowserUpload(env, directory, shipped)).toThrow('匹配的 source map');
// 这条用例在测试里跑一整遍正式构建:拆出 chunk 后每个都要核对 Debug ID,Linux runner 上实测 45 秒。
}, 120_000);
it('上传入口在没有明确环境配置时拒绝启动 CLI', () => {
expect(() => prepareBrowserUpload({})).toThrow('SENTRY_AUTH_TOKEN');
expect(() => prepareBrowserUpload({ SENTRY_AUTH_TOKEN: 'test-only' })).toThrow('SENTRY_ORG');
expect(() => prepareBrowserUpload({ SENTRY_AUTH_TOKEN: 'test-only', SENTRY_ORG: 'fixture' })).toThrow('SENTRY_PROJECT');
});