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,97 +1,23 @@
import * as assert from 'node:assert/strict';
import * as fs from 'node:fs';
import * as path from 'node:path';
import {
findInjectedDebugIds,
findSourceMapFiles,
getArtifactBundles,
getAssembleRequests,
getChunkUploadPosts,
getDebugIdPairs,
getSourcemaps,
loadMockServerResults,
} from '@sentry-internal/test-utils';
import { assertSourceMapUpload } from '@sentry-internal/test-utils';

const isStatic = process.env.E2E_ASTRO_OUTPUT === 'static';

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

/** All markers sit in comments, so bundlers strip them from the code but keep them in `sourcesContent`. */
const CLIENT_MARKER = 'SOURCEMAP_MARKER_CLIENT';
const SERVER_MARKER = 'SOURCEMAP_MARKER_SERVER';
const PRERENDER_MARKER = 'SOURCEMAP_MARKER_PRERENDER';

/** Written by the build, see `recordChunksWithoutSourceMap` in `astro.config.mjs`. */
const debugIdsWithoutSourceMap = new Set(
fs.existsSync('.tmp_debug_ids_without_source_map')
? fs.readFileSync('.tmp_debug_ids_without_source_map', 'utf8').split('\n').filter(Boolean)
: [],
);

const requests = loadMockServerResults();

console.log(`Captured ${requests.length} requests to mock Sentry server:\n`);
for (const request of requests) {
console.log(` ${request.method} ${request.url} (${request.bodySize} bytes)`);
}
console.log('');

assert.ok(
requests.some(r => r.authorization.includes('fake-auth-token')),
'Expected requests with the configured auth token',
);

assert.ok(
getChunkUploadPosts(requests).some(r => r.bodySize > 0),
'Expected at least one chunk upload POST with a non-empty body',
);

const assembleRequests = getAssembleRequests(requests);
assert.ok(assembleRequests.length > 0, 'Expected at least one assemble request');
for (const request of assembleRequests) {
assert.ok(request.assembleBody?.projects?.includes('test-project'), 'Expected assemble request for test-project');
assert.equal(request.assembleBody?.version, 'test-release', 'Expected assemble request to reference the release');
}

const bundles = getArtifactBundles(requests);
assert.ok(bundles.length > 0, 'Expected at least one artifact bundle with a manifest');

const sourcemaps = getSourcemaps(bundles);

const containsMarker = (marker: string): boolean =>
sourcemaps.some(map => map.sourcemap.sourcesContent?.some(source => source?.includes(marker)));

assert.ok(containsMarker(CLIENT_MARKER), 'Expected an uploaded sourcemap carrying the client source');
if (!isStatic) {
assert.ok(containsMarker(SERVER_MARKER), 'Expected an uploaded sourcemap carrying the server source');
}
// Astro 7 writes the prerender build to a hidden `.prerender` folder, which a plain `dist/**/*` glob skips
assert.ok(containsMarker(PRERENDER_MARKER), 'Expected an uploaded sourcemap carrying the prerendered page source');

const buildOutput = ['.tmp_build_stdout', '.tmp_build_stderr']
.filter(file => fs.existsSync(file))
.map(file => fs.readFileSync(file, 'utf8'))
.join('\n');
assert.ok(
!buildOutput.includes("Didn't find any matching sources for debug ID upload"),
'Expected every Vite build to find sources for the debug ID upload',
);

const uploadedDebugIds = new Set(getDebugIdPairs(bundles).map(pair => pair.debugId.toLowerCase()));
assert.ok(uploadedDebugIds.size > 0, 'Expected at least one JS/sourcemap pair with matching debug IDs');

for (const outputDir of OUTPUT_DIRS) {
const injectedDebugIds = findInjectedDebugIds({ outputDir });
assert.ok(injectedDebugIds.length > 0, `Expected debug IDs to be injected into ${outputDir}`);

const unuploaded = injectedDebugIds.filter(
debugId => !uploadedDebugIds.has(debugId) && !debugIdsWithoutSourceMap.has(debugId),
);
assert.deepEqual(unuploaded, [], `Expected every debug ID in ${outputDir} to have an uploaded sourcemap`);
}

// The app does not set `build.sourcemap`, so the SDK enables hidden maps and must delete them after upload.
assert.deepEqual(findSourceMapFiles({ outputDir: BUILD_OUTPUT }), [], `Expected no source maps in ${BUILD_OUTPUT}`);
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 @@ -2,19 +2,15 @@ import * as assert from 'assert/strict';
import * as fs from 'fs';
import * as path from 'path';
import {
assertSourceMapUpload,
findDebugIdsWithConflictingSourcemaps,
findFilesWithForeignSourcemaps,
findFilesWithMultipleDebugIds,
findInjectedDebugIds,
findSourceMapFiles,
findSourceMappingUrlComments,
getArtifactBundles,
getAssembleRequests,
getChunkUploadPosts,
getDebugIdPairs,
getSourcemapSources,
getSourcemaps,
loadMockServerResults,
startNodeServer,
} from '@sentry-internal/test-utils';

Expand Down Expand Up @@ -86,20 +82,22 @@ console.log(
`client source maps ${keepClientSourceMaps ? 'kept' : 'deleted'}\n`,
);

const requests = loadMockServerResults();

console.log(`Captured ${requests.length} requests to mock Sentry server:\n`);
for (const request of requests) {
console.log(` ${request.method} ${request.url} (${request.bodySize} bytes)`);
}
console.log('');

// --- The upload reached Sentry ---
const { requests, bundles, sourcemaps, uploadedDebugIds } = assertSourceMapUpload({
outputDirs: isStaticBuild ? [CLIENT_OUTPUT] : [CLIENT_OUTPUT, SERVER_OUTPUT],
// The client output is checked below, because the "kept" variant expects its maps to stay and some presets
// write the server into it.
strippedDirs: [],
// Vite builds the client and Nitro's Rollup builds the server. Counting bundles would still pass
// with either plugin dropped, so each side is pinned to a marker only that side's source supplies.
// Nitro defaults to `sourcemapExcludeSources: true`; the module flips it to `false`, which is the
// only reason the server marker survives into `sourcesContent`.
markers: { 'client source (Vite plugin)': CLIENT_MARKER, 'server source (Rollup plugin)': SERVER_MARKER },
// Nitro copies Vite's client output to `CLIENT_OUTPUT`, and Sentry deletes no maps in Vite's own directory.
debugIdsWithoutSourceMap: debugIdsOfChunksWithoutSourceMap(CLIENT_BUILD_OUTPUT),
});
console.log(`Found ${bundles.length} artifact bundle(s)\n`);

assert.ok(
requests.some(r => r.authorization.includes('fake-auth-token')),
'Expected requests with the configured auth token',
);
// --- Nuxt-specific checks on the upload ---

// Sentry rejects a release that names no project with a 400, and the mock server accepts anything,
// so assert on the body rather than on the call alone.
Expand Down Expand Up @@ -129,79 +127,23 @@ assert.ok(
'Expected a GET for the chunk-upload options',
);

const chunkPosts = getChunkUploadPosts(requests);
assert.ok(
chunkPosts.some(r => r.bodySize > 0),
'Expected at least one chunk upload POST with a non-empty body',
);

const assembleRequests = getAssembleRequests(requests);
assert.ok(assembleRequests.length > 0, 'Expected at least one assemble request');
for (const request of assembleRequests) {
assert.ok(request.assembleBody?.projects?.includes('test-project'), 'Expected assemble request for test-project');
assert.equal(request.assembleBody?.version, 'test-release', 'Expected assemble request to reference the release');
for (const request of getAssembleRequests(requests)) {
assert.ok((request.assembleBody?.chunks?.length ?? 0) > 0, 'Expected assemble request to have chunk checksums');
for (const chunk of request.assembleBody?.chunks ?? []) {
assert.match(chunk, /^[\da-f]{40}$/i, `Expected a SHA-1 chunk checksum, got: ${chunk}`);
}
}

const bundles = getArtifactBundles(requests);
assert.ok(bundles.length > 0, 'Expected at least one artifact bundle with a manifest');
console.log(`Found ${bundles.length} artifact bundle(s)\n`);

// --- Both bundlers uploaded ---

const sourcemaps = getSourcemaps(bundles);
assert.ok(
sourcemaps.some(map => map.sourcemap.mappings?.length),
'Expected at least one uploaded sourcemap with non-empty mappings',
);

const containsMarker = (marker: string): boolean =>
sourcemaps.some(map => map.sourcemap.sourcesContent?.some(source => source?.includes(marker)));

// Vite builds the client and Nitro's Rollup builds the server. Counting bundles would still pass
// with either plugin dropped, so each side is pinned to a marker only that side's source supplies.
assert.ok(containsMarker(CLIENT_MARKER), 'Expected an uploaded sourcemap carrying the client source (Vite plugin)');
// Nitro defaults to `sourcemapExcludeSources: true`; the module flips it to `false`, which is the
// only reason this marker survives into `sourcesContent`.
assert.ok(containsMarker(SERVER_MARKER), 'Expected an uploaded sourcemap carrying the server source (Rollup plugin)');

// `rewriteSources` normalizes `../../../foo` to `./foo` so paths stay resolvable in Sentry.
const unnormalizedSources = [...new Set(sourcemaps.flatMap(map => map.sourcemap.sources ?? []))].filter(
source => source.startsWith('../') || path.isAbsolute(source),
);
assert.deepEqual(unnormalizedSources, [], `Expected every uploaded source to be normalized to './…'`);

// --- Debug IDs tie the shipped bundle to the uploaded map ---

const uploadedDebugIds = new Set(getDebugIdPairs(bundles).map(pair => pair.debugId.toLowerCase()));
assert.ok(uploadedDebugIds.size > 0, 'Expected at least one JS/sourcemap pair with matching debug IDs');

const malformedDebugIds = [...uploadedDebugIds].filter(debugId => !UUID_REGEX.test(debugId));
assert.deepEqual(malformedDebugIds, [], 'Expected every uploaded debug ID to be a UUID');

// An uploaded map is only reachable at runtime if the shipped bundle claims the same ID. Inspecting
// the upload alone cannot show this.
const debugIdsWithoutSourceMap = debugIdsOfChunksWithoutSourceMap(CLIENT_BUILD_OUTPUT);

for (const outputDir of isStaticBuild ? [CLIENT_OUTPUT] : [CLIENT_OUTPUT, SERVER_OUTPUT]) {
const injectedDebugIds = findInjectedDebugIds({ outputDir });
assert.ok(injectedDebugIds.length > 0, `Expected debug IDs to be injected into ${outputDir}`);
// The exemption for chunks without a source map must not let a build without any uploaded map pass.
assert.ok(
injectedDebugIds.some(debugId => uploadedDebugIds.has(debugId)),
`Expected at least one debug ID in ${outputDir} to have an uploaded sourcemap`,
);

const unuploaded = injectedDebugIds.filter(
debugId => !uploadedDebugIds.has(debugId) && !debugIdsWithoutSourceMap.has(debugId),
);
assert.deepEqual(unuploaded, [], `Expected every debug ID in ${outputDir} to have an uploaded sourcemap`);

console.log(` ${outputDir}: ${injectedDebugIds.length} injected debug ID(s), all uploaded`);

// Nitro bundles the SSR build again, so its debug IDs can end up next to Nitro's own.
// Reference issue: https://github.com/getsentry/sentry-javascript/issues/18519
const filesWithMultipleDebugIds = findFilesWithMultipleDebugIds({ outputDir });
Expand Down
Loading
Loading