Skip to content

Commit ae748ec

Browse files
chargomeclaude
andauthored
test(remix): Add Remix 3 e2e test application (#24698)
Adds a Remix 3 app to the e2e matrix, stacked on #24697. There is no instrumentation yet, so the two smoke tests assert only that the plumbing holds: the app boots under `--import @sentry/remix/v3/node`, and `Sentry.init()` from the `@sentry/remix/v3` subpath sends a server span. Span names are still URL based. The app exists at this point in the stack so later pull requests add instrumentation and its tests together, instead of also introducing a new CI surface. Each of those restores its own test file. The browser entry calls no Sentry API, because `./v3/client` withholds `init` until the browser SDK lands. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 59a57a7 commit ae748ec

11 files changed

Lines changed: 194 additions & 0 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { createController } from 'remix/router';
2+
import type { Handle } from 'remix/ui';
3+
4+
import { assets, entryHref, entryPreloads } from '../assets.ts';
5+
import { routes } from '../routes.ts';
6+
7+
function HomePage(handle: Handle<Record<string, never>>) {
8+
return () => (
9+
<html lang="en">
10+
<head>
11+
<meta charSet="utf-8" />
12+
<title>Sentry Remix 3</title>
13+
{entryPreloads.map(href => (
14+
<link key={href} rel="modulepreload" href={href} />
15+
))}
16+
<script type="module" src={entryHref}></script>
17+
</head>
18+
<body>
19+
<h1 id="home">Sentry Remix 3</h1>
20+
</body>
21+
</html>
22+
);
23+
}
24+
25+
export default createController(routes, {
26+
actions: {
27+
async assets(context) {
28+
return (await assets.fetch(context.request)) ?? new Response('Not Found', { status: 404 });
29+
},
30+
home(context) {
31+
return context.render(<HomePage />);
32+
},
33+
},
34+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { run } from 'remix/ui';
2+
3+
// No Sentry here yet: `@sentry/remix/v3/client` does not export `init` until the browser SDK lands.
4+
export const app = run({
5+
async loadModule(moduleUrl, exportName) {
6+
let mod = await import(moduleUrl);
7+
return mod[exportName];
8+
},
9+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { createAssetServer } from 'remix/assets';
2+
3+
export const assets = createAssetServer({
4+
basePath: '/assets',
5+
rootDir: process.cwd(),
6+
allowFiles: ['app/routes.ts', 'app/**/public/**'],
7+
allowPackages: ['remix', '@sentry/remix'],
8+
minify: true,
9+
watch: false,
10+
});
11+
12+
const entry = 'app/actions/public/entry.ts';
13+
14+
export const entryHref = await assets.getHref(entry);
15+
export const entryPreloads = await assets.getPreloads(entry);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { createRouter, type MiddlewareContext } from 'remix/router';
2+
import { render } from 'remix/middleware/render';
3+
4+
import controller from './actions/controller.tsx';
5+
import { assets } from './assets.ts';
6+
import { routes } from './routes.ts';
7+
8+
const renderMiddleware = render({ assets });
9+
type AppContext = MiddlewareContext<[typeof renderMiddleware]>;
10+
11+
declare module 'remix/router' {
12+
interface RouterTypes {
13+
context: AppContext;
14+
}
15+
}
16+
17+
export const router = createRouter<AppContext>({
18+
middleware: [renderMiddleware],
19+
});
20+
21+
router.map(routes, controller);
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { get, route } from 'remix/routes';
2+
3+
export const routes = route({
4+
assets: get('/assets/*path'),
5+
home: '/',
6+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "remix-v3-app",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"start": "NODE_ENV=production node --import @sentry/remix/v3/node server.ts",
8+
"test": "playwright test",
9+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
10+
"test:build": "pnpm install",
11+
"test:assert": "pnpm test"
12+
},
13+
"dependencies": {
14+
"@sentry/remix": "file:../../packed/sentry-remix-packed.tgz",
15+
"remix": "3.0.0-rc.1"
16+
},
17+
"devDependencies": {
18+
"@playwright/test": "~1.56.0",
19+
"@sentry-internal/test-utils": "link:../../../test-utils",
20+
"@sentry/core": "file:../../packed/sentry-core-packed.tgz",
21+
"@types/node": "^24.6.0",
22+
"typescript": "~5.9.0"
23+
},
24+
"volta": {
25+
"node": "24.18.0",
26+
"pnpm": "9.15.9"
27+
}
28+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: `pnpm start`,
5+
port: 3060,
6+
eventProxyPort: 3061,
7+
});
8+
9+
export default config;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as Sentry from '@sentry/remix/v3';
2+
import * as http from 'node:http';
3+
import { createRequestListener } from 'remix/node-fetch-server';
4+
5+
import { router } from './app/router.ts';
6+
7+
Sentry.init({
8+
dsn: process.env.E2E_TEST_DSN,
9+
tunnel: 'http://localhost:3061/',
10+
tracesSampleRate: 1.0,
11+
});
12+
13+
const port = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 3060;
14+
15+
const server = http.createServer(
16+
createRequestListener(async request => {
17+
try {
18+
return await router.fetch(request);
19+
} catch {
20+
return new Response('Internal Server Error', { status: 500 });
21+
}
22+
}),
23+
);
24+
25+
server.listen(port, () => {
26+
console.log(`Server listening on http://localhost:${port}`);
27+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { startEventProxyServer } from '@sentry-internal/test-utils';
2+
3+
startEventProxyServer({
4+
port: 3061,
5+
proxyServerName: 'remix-v3',
6+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForStreamedSpan } from '@sentry-internal/test-utils';
3+
4+
// There is no instrumentation yet. This app exists so later pull requests add instrumentation and its
5+
// tests together, rather than also introducing a new CI surface.
6+
test('the app boots under the Sentry --import entry', async ({ page }) => {
7+
await page.goto('/');
8+
9+
await expect(page.locator('#home')).toBeVisible();
10+
});
11+
12+
test('Sentry.init from the v3 subpath reports a server span', async ({ baseURL }) => {
13+
// Names are still URL based. This only proves the subpath resolves and the SDK is live.
14+
const spanPromise = waitForStreamedSpan('remix-v3', span => span.is_segment === true);
15+
16+
await fetch(`${baseURL}/`);
17+
18+
const span = await spanPromise;
19+
expect(span.attributes?.['http.request.method']?.value).toBe('GET');
20+
});

0 commit comments

Comments
 (0)