Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,23 +1,53 @@
import * as assert from 'node:assert/strict';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { assertSourceMapUpload } from '@sentry-internal/test-utils';
import {
assertSourceMapUpload,
findInjectedDebugIds,
findSourceMapFiles,
findSourceMappingUrlComments,
} from '@sentry-internal/test-utils';

const isStatic = process.env.E2E_ASTRO_OUTPUT === 'static';
const noUpload = !!process.env.E2E_NO_UPLOAD;

const BUILD_OUTPUT = 'dist';
const BUILD_OUTPUT = noUpload ? 'build' : 'dist';
const OUTPUT_DIRS = isStatic ? [BUILD_OUTPUT] : [path.join(BUILD_OUTPUT, 'client'), path.join(BUILD_OUTPUT, 'server')];

assertSourceMapUpload({
outputDirs: OUTPUT_DIRS,
// The app does not set `build.sourcemap`, so the SDK enables hidden maps and must delete them after upload.
strippedDirs: [BUILD_OUTPUT],
markers: {
'client source': 'SOURCEMAP_MARKER_CLIENT',
...(isStatic ? {} : { 'server source': 'SOURCEMAP_MARKER_SERVER' }),
// Astro 7 writes the prerender build to a hidden `.prerender` folder, which a plain `dist/**/*` glob skips
'prerendered page source': 'SOURCEMAP_MARKER_PRERENDER',
},
// Written by the build, see `recordChunksWithoutSourceMap` in `astro.config.mjs`.
debugIdsWithoutSourceMap: '.tmp_debug_ids_without_source_map',
});
// The SDK enables hidden source maps when the app sets none, and asks the bundler plugin to remove
// them from the client, server and prerender output, also when no upload runs.
if (noUpload) {
// Written by the build, see `recordSourceMaps` in `astro.config.mjs`.
const mapsMade = new Map<string, number>();
for (const line of fs.readFileSync('.tmp_source_maps_made', 'utf8').split('\n').filter(Boolean)) {
const [environment, count] = line.split(' ');
mapsMade.set(environment as string, (mapsMade.get(environment as string) ?? 0) + Number(count));
}
for (const environment of ['client', 'ssr', 'prerender']) {
assert.ok(mapsMade.get(environment), `Expected the ${environment} build to make source maps`);
}
assert.notDeepEqual(findInjectedDebugIds({ outputDir: BUILD_OUTPUT }), [], `Expected debug IDs in ${BUILD_OUTPUT}`);

assert.deepEqual(findSourceMapFiles({ outputDir: BUILD_OUTPUT }), [], `Expected no source maps in ${BUILD_OUTPUT}`);
assert.deepEqual(
findSourceMappingUrlComments({ outputDir: BUILD_OUTPUT }),
[],
`Expected no sourceMappingURL comments in ${BUILD_OUTPUT}`,
);
} else {
assertSourceMapUpload({
outputDirs: OUTPUT_DIRS,
// The app does not set `build.sourcemap`, so the SDK enables hidden maps and must delete them after upload.
strippedDirs: [BUILD_OUTPUT],
markers: {
'client source': 'SOURCEMAP_MARKER_CLIENT',
...(isStatic ? {} : { 'server source': 'SOURCEMAP_MARKER_SERVER' }),
// Astro 7 writes the prerender build to a hidden `.prerender` folder, which a plain `dist/**/*` glob skips
'prerendered page source': 'SOURCEMAP_MARKER_PRERENDER',
},
// Written by the build, see `recordChunksWithoutSourceMap` in `astro.config.mjs`.
debugIdsWithoutSourceMap: '.tmp_debug_ids_without_source_map',
});
}

console.log('All sourcemap assertions passed!');
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ import { defineConfig } from 'astro/config';
import * as fs from 'node:fs';

const isStatic = process.env.E2E_ASTRO_OUTPUT === 'static';
const noUpload = !!process.env.E2E_NO_UPLOAD;

// Rolldown emits no map for chunks without mappable source (e.g. virtual modules or its runtime), so their debug
// IDs can't be uploaded. Recorded here because the SDK deletes all maps after the upload.
function recordChunksWithoutSourceMap() {
// Records the source maps each environment made, before the Sentry plugin removes them in its
// `generateBundle` with `order: 'post'`. Rolldown emits no map for chunks without mappable source
// (e.g. virtual modules or its runtime), so their debug IDs can't be uploaded.
function recordSourceMaps() {
return {
name: 'record-chunks-without-source-map',
name: 'record-source-maps',
apply: 'build',
enforce: 'post',
writeBundle(_options, bundle) {
generateBundle(_options, bundle) {
const maps = Object.keys(bundle).filter(fileName => fileName.endsWith('.map'));
fs.appendFileSync('.tmp_source_maps_made', `${this.environment.name} ${maps.length}\n`);

for (const output of Object.values(bundle)) {
if (output.type !== 'chunk' || bundle[`${output.fileName}.map`]) {
continue;
Expand All @@ -39,7 +43,7 @@ export default defineConfig({
integrations: [
sentry({
sentryUrl: 'http://localhost:3032',
authToken: 'fake-auth-token',
authToken: noUpload ? undefined : 'fake-auth-token',
org: 'test-org',
project: 'test-project',
release: { name: 'test-release' },
Expand All @@ -48,7 +52,9 @@ export default defineConfig({
...(isStatic ? [] : [ssrRoute]),
],
// Keeps Astro from inlining the small client script into the HTML, where it would get no source map
vite: { build: { assetsInlineLimit: 0 }, plugins: [recordChunksWithoutSourceMap()] },
vite: { build: { assetsInlineLimit: 0 }, plugins: [recordSourceMaps()] },
// The removal must follow the configured output directory
...(noUpload && { outDir: './build' }),
output: isStatic ? 'static' : 'server',
adapter: isStatic ? undefined : node({ mode: 'standalone' }),
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
"test:build": "pnpm install && pnpm build",
"test:assert": "node assert-build.ts",
"test:build:static": "E2E_ASTRO_OUTPUT=static pnpm test:build",
"test:assert:static": "E2E_ASTRO_OUTPUT=static pnpm test:assert"
"test:assert:static": "E2E_ASTRO_OUTPUT=static pnpm test:assert",
"test:build:no-upload": "E2E_NO_UPLOAD=1 pnpm test:build",
"test:assert:no-upload": "E2E_NO_UPLOAD=1 pnpm test:assert"
},
"dependencies": {
"@astrojs/node": "^11.1.4",
Expand All @@ -26,6 +28,11 @@
"build-command": "pnpm test:build:static",
"assert-command": "pnpm test:assert:static",
"label": "astro-7-sourcemaps (static output)"
},
{
"build-command": "pnpm test:build:no-upload",
"assert-command": "pnpm test:assert:no-upload",
"label": "astro-7-sourcemaps (no upload)"
}
]
}
Expand Down
58 changes: 33 additions & 25 deletions packages/astro/src/integration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,36 +67,23 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
if (shouldUploadSourcemaps && command !== 'dev') {
const computedSourceMapSettings = _getUpdatedSourceMapSettings(config, options, logger);

let updatedFilesToDeleteAfterUpload: string[] | undefined = undefined;

if (
typeof sourcemaps?.filesToDeleteAfterUpload === 'undefined' &&
computedSourceMapSettings.previousUserSourceMapSetting === 'unset'
) {
// This also works for adapters, as the source maps are also copied to e.g. the .vercel folder.
// Static output has no client folder and writes client assets to `<outDir>/<build.assets>` instead.
// Astro removes the hidden `.prerender` folder itself, but we don't want to rely on that.
updatedFilesToDeleteAfterUpload = [
'./dist/**/client/**/*.map',
'./dist/**/server/**/*.map',
`./dist/**/${config.build?.assets ?? '_astro'}/**/*.map`,
'./dist/**/.prerender/**/*.map',
];

debug &&
logger.info(
`Automatically setting \`sourcemaps.filesToDeleteAfterUpload: ${JSON.stringify(
updatedFilesToDeleteAfterUpload,
)}\` to delete generated source maps after they were uploaded to Sentry.`,
);
}
const sdkEnabledSourceMaps = computedSourceMapSettings.previousUserSourceMapSetting === 'unset';
const buildState: ViteBuildState = { writesCloudflareWorker: false };
// A user's own glob replaces the SDK's deletion. Maps in a Cloudflare `_worker.js` folder
// stay, so that wrangler can read them for `upload_source_maps`.
const deleteSourcemapsAfterUpload =
sourcemaps?.deleteSourcemapsAfterUpload ??
(sdkEnabledSourceMaps && sourcemaps?.filesToDeleteAfterUpload === undefined
? () => (buildState.writesCloudflareWorker ? undefined : true)
: undefined);

updateConfig({
vite: {
build: {
sourcemap: computedSourceMapSettings.updatedSourceMapSetting,
},
plugins: [
makeViteBuildStatePlugin(buildState),
sentryVitePlugin({
applicationKey,
moduleMetadata,
Expand All @@ -119,7 +106,8 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
sourcemaps: {
...sourcemaps,
assets: sourcemaps?.assets ?? getSourcemapsAssetsGlobs(config),
filesToDeleteAfterUpload: sourcemaps?.filesToDeleteAfterUpload ?? updatedFilesToDeleteAfterUpload,
deleteSourcemapsAfterUpload,
sdkEnabledSourceMaps,
},
bundleSizeOptimizations: {
...bundleSizeOptimizations,
Expand Down Expand Up @@ -274,6 +262,26 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
};
};

type ViteBuildState = {
/** The current Vite build writes the Worker of an older Cloudflare adapter to `_worker.js`. */
writesCloudflareWorker: boolean;
};

/**
* Records what the SDK needs to know about each Vite build. Its `config` hook runs before the one
* of the Sentry plugin, which reads `buildState` there.
*/
function makeViteBuildStatePlugin(buildState: ViteBuildState): VitePlugin {
return {
name: 'sentry-astro-vite-build-state',
apply: 'build',
enforce: 'post',
config(viteConfig) {
buildState.writesCloudflareWorker = /(?:^|[\\/])_worker\.js[\\/]*$/.test(viteConfig.build?.outDir ?? '');
},
};
}

// Every sub-plugin of `sentryCloudflareVitePlugin()` but its orchestrion transform has this prefix,
// and some are added whatever its options are.
const CLOUDFLARE_VITE_PLUGIN_NAME_PREFIX = 'sentry-cloudflare-';
Expand Down Expand Up @@ -390,7 +398,7 @@ export type UserSourceMapSetting = 'enabled' | 'disabled' | 'unset' | undefined;
*
* 3. Users didn't set source maps generation
* - we enable 'hidden' source maps generation
* - configure `filesToDeleteAfterUpload` to delete all .map files (we emit a log about this)
* - remove the maps from the build output, except in a Cloudflare `_worker.js` folder
*
* --> only exported for testing
*/
Expand Down
Loading
Loading