Repository navigation
test(tanstackstart-react): Add source map E2E app #25363
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
s1gr1d
wants to merge
3
commits into
sig/tanstack-start-source-maps
Choose a base branch
from
sig/tanstack-start-source-maps-e2e
base: sig/tanstack-start-source-maps
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+324
−0
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
11 changes: 11 additions & 0 deletions
11
dev-packages/e2e-tests/test-applications/tanstackstart-react-sourcemaps/.gitignore
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| node_modules | ||
| .output | ||
| .nitro | ||
| .tanstack | ||
| src/routeTree.gen.ts | ||
|
|
||
| # Mock Sentry server artifacts | ||
| .tmp_mock_uploads.json | ||
| .tmp_chunks | ||
| .tmp_build_stdout | ||
| .tmp_build_stderr |
191 changes: 191 additions & 0 deletions
191
dev-packages/e2e-tests/test-applications/tanstackstart-react-sourcemaps/assert-build.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| import * as assert from 'assert/strict'; | ||
| import * as fs from 'fs'; | ||
| import * as path from 'path'; | ||
| import { | ||
| findDebugIdsWithConflictingSourcemaps, | ||
| findFilesWithForeignSourcemaps, | ||
| findFilesWithMultipleDebugIds, | ||
| findInjectedDebugIds, | ||
| getArtifactBundles, | ||
| getChunkUploadPosts, | ||
| getDebugIdPairs, | ||
| getSourcemapSources, | ||
| getSourcemaps, | ||
| loadMockServerResults, | ||
| startNodeServer, | ||
| type SourcemapEntry, | ||
| } from '@sentry-internal/test-utils'; | ||
|
|
||
| const CLIENT_OUTPUT = path.join('.output', 'public'); | ||
| const SERVER_OUTPUT = path.join('.output', 'server'); | ||
|
|
||
| /** Both 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 DEBUG_ID_PATTERN = /sentry-dbid-([\da-f-]{36})/gi; | ||
|
|
||
| function readDebugIds(file: string): string[] { | ||
| return [...fs.readFileSync(file, 'utf8').matchAll(DEBUG_ID_PATTERN)].map(match => (match[1] as string).toLowerCase()); | ||
| } | ||
|
|
||
| function emittedJsFiles(dir: string): string[] { | ||
| return fs | ||
| .readdirSync(dir, { recursive: true, withFileTypes: true }) | ||
| .filter(entry => entry.isFile() && /\.m?js$/.test(entry.name) && !entry.parentPath.includes('node_modules')) | ||
| .map(entry => path.join(entry.parentPath, entry.name)); | ||
| } | ||
|
|
||
| /** In debug mode, the Sentry plugin logs each file it deletes after the upload. */ | ||
| const deletedFiles = new Set( | ||
| [...fs.readFileSync('.tmp_build_stdout', 'utf8').matchAll(/Deleting asset after upload: (.+)$/gm)].map(match => | ||
| path.relative(fs.realpathSync('.'), match[1] as string), | ||
| ), | ||
| ); | ||
|
|
||
| /** A chunk without mappable source, such as Nitro's runtime, gets a debug ID but no source map to upload. */ | ||
| function debugIdsOfChunksWithoutSourceMap(dir: string): Set<string> { | ||
| return new Set( | ||
| emittedJsFiles(dir) | ||
| .filter(file => !fs.existsSync(`${file}.map`) && !deletedFiles.has(`${file}.map`)) | ||
| .flatMap(readDebugIds), | ||
| ); | ||
| } | ||
|
|
||
| function sourceMapsIn(dir: string): string[] { | ||
| return fs | ||
| .readdirSync(dir, { recursive: true, withFileTypes: true }) | ||
| .filter(entry => entry.isFile() && entry.name.endsWith('.map')) | ||
| .map(entry => path.join(entry.parentPath, entry.name)); | ||
| } | ||
|
|
||
| // TODO: The uploader passes the emitted file paths to `glob` as patterns, so it skips a file whose path has glob | ||
| // characters, like Nitro's `_libs/@tanstack/react-router+[...].mjs`. Remove this when the uploader matches paths literally. | ||
| function debugIdsOfFilesWithGlobCharacters(dir: string): Set<string> { | ||
| return new Set( | ||
| emittedJsFiles(dir) | ||
| .filter(file => /[[\]{}()*?]/.test(path.relative(dir, file))) | ||
| .flatMap(readDebugIds), | ||
| ); | ||
| } | ||
|
|
||
| const requests = loadMockServerResults(); | ||
|
|
||
| // --- The upload reached Sentry --- | ||
|
|
||
| assert.ok( | ||
| getChunkUploadPosts(requests).some(request => request.bodySize > 0), | ||
| 'Expected at least one chunk upload with a non-empty body', | ||
| ); | ||
|
|
||
| const bundles = getArtifactBundles(requests); | ||
| assert.ok(bundles.length > 0, 'Expected at least one artifact bundle with a manifest'); | ||
|
|
||
| const sourcemaps = getSourcemaps(bundles); | ||
|
|
||
| /** The uploaded source maps of the files in an output directory, matched by debug ID. */ | ||
| function sourceMapsOf(outputDir: string): SourcemapEntry[] { | ||
| const debugIds = new Set(findInjectedDebugIds({ outputDir })); | ||
| return sourcemaps.filter(map => map.debugId && debugIds.has(map.debugId)); | ||
| } | ||
|
|
||
| const clientSourceMaps = sourceMapsOf(CLIENT_OUTPUT); | ||
| const serverSourceMaps = sourceMapsOf(SERVER_OUTPUT); | ||
| const containsMarker = (maps: SourcemapEntry[], marker: string): boolean => | ||
| maps.some(map => map.sourcemap.sourcesContent?.some(source => source?.includes(marker))); | ||
|
|
||
| // Both markers are in the same route file, which client and server source maps embed completely. So each marker only | ||
| // proves the upload of its side if the check only looks at the source maps of that side. | ||
| assert.ok(containsMarker(clientSourceMaps, CLIENT_MARKER), 'Expected a client source map carrying the client source'); | ||
| assert.ok(containsMarker(serverSourceMaps, SERVER_MARKER), 'Expected a server source map carrying the server source'); | ||
|
|
||
| // --- Source paths match the files in the project --- | ||
|
|
||
| const unresolvedSources = [...new Set(sourcemaps.flatMap(map => getSourcemapSources(map.sourcemap)))].filter( | ||
| source => source.startsWith('../') || path.isAbsolute(source) || /\?ts[rs]-/.test(source), | ||
| ); | ||
| assert.deepEqual(unresolvedSources, [], 'Expected every uploaded source path to be relative to the project root'); | ||
|
|
||
| // --- Server source maps reach the original source --- | ||
|
|
||
| // Nitro bundles the SSR output again. Its maps must lead through that output to the file the server function is in. | ||
| const serverFunctionSources = serverSourceMaps | ||
| .filter(map => map.sourcemap.sourcesContent?.some(source => source?.includes(SERVER_MARKER))) | ||
| .flatMap(map => getSourcemapSources(map.sourcemap)); | ||
| assert.ok( | ||
| serverFunctionSources.includes('src/routes/index.tsx'), | ||
| `Expected a server source map to resolve to src/routes/index.tsx, got: ${serverFunctionSources.join(', ')}`, | ||
| ); | ||
|
|
||
| // --- Debug IDs tie the shipped bundles to the uploaded maps --- | ||
|
|
||
| const uploadedDebugIds = new Set(getDebugIdPairs(bundles).map(pair => pair.debugId.toLowerCase())); | ||
|
|
||
| for (const outputDir of [CLIENT_OUTPUT, SERVER_OUTPUT]) { | ||
| const injectedDebugIds = findInjectedDebugIds({ outputDir }); | ||
| assert.ok(injectedDebugIds.length > 0, `Expected debug IDs to be injected into ${outputDir}`); | ||
|
|
||
| const exemptDebugIds = new Set([ | ||
| ...debugIdsOfChunksWithoutSourceMap(outputDir), | ||
| ...debugIdsOfFilesWithGlobCharacters(outputDir), | ||
| ]); | ||
| const unuploaded = injectedDebugIds.filter(debugId => !uploadedDebugIds.has(debugId) && !exemptDebugIds.has(debugId)); | ||
| assert.deepEqual(unuploaded, [], `Expected every debug ID in ${outputDir} to have an uploaded source map`); | ||
|
|
||
| // A file of Nitro's output can carry the debug ID of the SSR chunk it was built from. | ||
| assert.deepEqual( | ||
| findFilesWithMultipleDebugIds({ outputDir }), | ||
| [], | ||
| `Expected at most one debug ID per file in ${outputDir}`, | ||
| ); | ||
| assert.deepEqual( | ||
| findFilesWithForeignSourcemaps({ outputDir }, sourcemaps), | ||
| [], | ||
| `Expected the source map uploaded for each file in ${outputDir} to be generated for that file`, | ||
| ); | ||
|
|
||
| console.log(` ${outputDir}: ${injectedDebugIds.length} injected debug ID(s)`); | ||
| } | ||
|
|
||
| assert.deepEqual( | ||
| findDebugIdsWithConflictingSourcemaps(sourcemaps), | ||
| [], | ||
| 'Expected every uploaded debug ID to resolve to exactly one source map', | ||
| ); | ||
|
|
||
| // --- Sentry deletes the source maps it enabled after the upload --- | ||
|
|
||
| for (const outputDir of [CLIENT_OUTPUT, SERVER_OUTPUT]) { | ||
| assert.deepEqual(sourceMapsIn(outputDir), [], `Expected no source maps in ${outputDir}`); | ||
| } | ||
|
|
||
| const clientChunkUrls = emittedJsFiles(CLIENT_OUTPUT).map( | ||
| file => `/${path.relative(CLIENT_OUTPUT, file).split(path.sep).join('/')}`, | ||
| ); | ||
| assert.ok(clientChunkUrls.length > 0, `Expected client chunks in ${CLIENT_OUTPUT}`); | ||
|
|
||
| const server = await startNodeServer({ entry: path.join(SERVER_OUTPUT, 'index.mjs'), port: 3030 }); | ||
|
|
||
| try { | ||
| // Without this, a server that serves no public files at all would pass the 404 checks. | ||
| const chunkStatus = (await fetch(`${server.baseUrl}${clientChunkUrls[0]}`)).status; | ||
| assert.equal(chunkStatus, 200, `Expected the server to serve the client chunk ${clientChunkUrls[0]}`); | ||
|
|
||
| // Nitro serves public files from a list it writes into the server bundle. A map deleted after that | ||
| // stays on the list, and Nitro answers its request with a 500 instead of a 404. | ||
| const responses = await Promise.all( | ||
| clientChunkUrls.map(async chunkUrl => ({ | ||
| mapUrl: `${chunkUrl}.map`, | ||
| status: (await fetch(`${server.baseUrl}${chunkUrl}.map`)).status, | ||
| })), | ||
| ); | ||
| assert.deepEqual( | ||
| responses.filter(({ status }) => status !== 404).map(({ mapUrl, status }) => `${mapUrl} → ${status}`), | ||
| [], | ||
| 'Expected the server to answer each client source map URL with 404', | ||
| ); | ||
| } finally { | ||
| await server.close(); | ||
| } | ||
|
|
||
| console.log('All source map assertions passed!'); | ||
37 changes: 37 additions & 0 deletions
37
dev-packages/e2e-tests/test-applications/tanstackstart-react-sourcemaps/package.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| { | ||
| "name": "tanstackstart-react-sourcemaps", | ||
| "description": "E2E test app asserting what the TanStack Start SDK uploads to Sentry when Nitro builds the server.", | ||
| "private": true, | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "node start-mock-sentry-server.mjs & vite build > .tmp_build_stdout 2> .tmp_build_stderr; BUILD_EXIT=$?; kill %1 2>/dev/null; if [ $BUILD_EXIT -ne 0 ]; then cat .tmp_build_stdout; cat .tmp_build_stderr >&2; fi; exit $BUILD_EXIT", | ||
| "clean": "npx rimraf node_modules pnpm-lock.yaml .output", | ||
| "test:build": "pnpm install && pnpm build", | ||
| "test:assert": "pnpm ts-node --script-mode assert-build.ts" | ||
| }, | ||
| "dependencies": { | ||
| "@sentry/tanstackstart-react": "file:../../packed/sentry-tanstackstart-react-packed.tgz", | ||
| "@tanstack/react-router": "1.170.44", | ||
| "@tanstack/react-start": "1.169.1", | ||
| "nitro": "latest", | ||
| "react": "^19.2.0", | ||
| "react-dom": "^19.2.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@playwright/test": "~1.63.0", | ||
| "@sentry-internal/test-utils": "link:../../../test-utils", | ||
| "@types/node": "^22.20.0", | ||
| "@types/react": "^19.2.0", | ||
| "@types/react-dom": "^19.2.0", | ||
| "@vitejs/plugin-react-swc": "^4.2.0", | ||
| "ts-node": "10.9.1", | ||
| "typescript": "^5.9.0", | ||
| "vite": "^8.3.0" | ||
| }, | ||
| "volta": { | ||
| "extends": "../../package.json" | ||
| }, | ||
| "sentryTest": { | ||
| "optional": true | ||
| } | ||
| } |
4 changes: 4 additions & 0 deletions
4
dev-packages/e2e-tests/test-applications/tanstackstart-react-sourcemaps/src/router.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import { createRouter } from '@tanstack/react-router'; | ||
| import { routeTree } from './routeTree.gen'; | ||
|
|
||
| export const getRouter = () => createRouter({ routeTree }); |
20 changes: 20 additions & 0 deletions
20
...packages/e2e-tests/test-applications/tanstackstart-react-sourcemaps/src/routes/__root.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import type { ReactNode } from 'react'; | ||
| import { createRootRoute, HeadContent, Outlet, Scripts } from '@tanstack/react-router'; | ||
|
|
||
| export const Route = createRootRoute({ | ||
| component: RootComponent, | ||
| }); | ||
|
|
||
| function RootComponent(): ReactNode { | ||
| return ( | ||
| <html> | ||
| <head> | ||
| <HeadContent /> | ||
| </head> | ||
| <body> | ||
| <Outlet /> | ||
| <Scripts /> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } |
21 changes: 21 additions & 0 deletions
21
dev-packages/e2e-tests/test-applications/tanstackstart-react-sourcemaps/src/routes/index.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { createFileRoute } from '@tanstack/react-router'; | ||
| import { createServerFn } from '@tanstack/react-start'; | ||
|
|
||
| // SOURCEMAP_MARKER_CLIENT — a comment, so it is stripped from the emitted bundles but survives in `sourcesContent`. | ||
| // `assert-build.ts` uses the markers to check that the original source reached the uploaded maps. | ||
| const throwServerError = createServerFn().handler(async () => { | ||
| // SOURCEMAP_MARKER_SERVER | ||
| throw new Error('Server function error from the TanStack Start source map E2E app'); | ||
|
s1gr1d marked this conversation as resolved.
|
||
| }); | ||
|
|
||
| export const Route = createFileRoute('/')({ | ||
| component: Home, | ||
| }); | ||
|
|
||
| function Home() { | ||
| return ( | ||
| <button type="button" onClick={() => throwServerError()}> | ||
| Throw server function error | ||
| </button> | ||
| ); | ||
| } | ||
3 changes: 3 additions & 0 deletions
3
...s/e2e-tests/test-applications/tanstackstart-react-sourcemaps/start-mock-sentry-server.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { startMockSentryServer } from '@sentry-internal/test-utils'; | ||
|
|
||
| startMockSentryServer({ org: 'test-org' }); |
16 changes: 16 additions & 0 deletions
16
dev-packages/e2e-tests/test-applications/tanstackstart-react-sourcemaps/tsconfig.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "target": "ES2022", | ||
| "lib": ["ES2022", "DOM", "DOM.Iterable"], | ||
| "module": "ESNext", | ||
| "moduleResolution": "bundler", | ||
| "jsx": "react-jsx", | ||
| "strict": true, | ||
| "skipLibCheck": true, | ||
| "esModuleInterop": true, | ||
| "resolveJsonModule": true, | ||
| "noEmit": true | ||
| }, | ||
| "include": ["**/*.ts", "**/*.tsx"], | ||
| "exclude": ["node_modules", ".output"] | ||
| } |
21 changes: 21 additions & 0 deletions
21
dev-packages/e2e-tests/test-applications/tanstackstart-react-sourcemaps/vite.config.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { sentryTanstackStart } from '@sentry/tanstackstart-react/vite'; | ||
| import { tanstackStart } from '@tanstack/react-start/plugin/vite'; | ||
| import viteReact from '@vitejs/plugin-react-swc'; | ||
| import { nitro } from 'nitro/vite'; | ||
| import { defineConfig } from 'vite'; | ||
|
|
||
| export default defineConfig({ | ||
| plugins: [ | ||
| tanstackStart(), | ||
| nitro(), | ||
| viteReact(), | ||
| sentryTanstackStart({ | ||
| sentryUrl: 'http://localhost:3032', | ||
| authToken: 'fake-auth-token', | ||
| org: 'test-org', | ||
| project: 'test-project', | ||
| release: { name: 'test-release' }, | ||
| debug: true, | ||
| }), | ||
| ], | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tracking issue for this: #25321