diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/controller.tsx b/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/controller.tsx new file mode 100644 index 000000000000..9325143b071d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/controller.tsx @@ -0,0 +1,34 @@ +import { createController } from 'remix/router'; +import type { Handle } from 'remix/ui'; + +import { assets, entryHref, entryPreloads } from '../assets.ts'; +import { routes } from '../routes.ts'; + +function HomePage(handle: Handle>) { + return () => ( + + + + Sentry Remix 3 + {entryPreloads.map(href => ( + + ))} + + + +

Sentry Remix 3

+ + + ); +} + +export default createController(routes, { + actions: { + async assets(context) { + return (await assets.fetch(context.request)) ?? new Response('Not Found', { status: 404 }); + }, + home(context) { + return context.render(); + }, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/public/entry.ts b/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/public/entry.ts new file mode 100644 index 000000000000..5f0ebdc1248e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/public/entry.ts @@ -0,0 +1,9 @@ +import { run } from 'remix/ui'; + +// No Sentry here yet: `@sentry/remix/v3/client` does not export `init` until the browser SDK lands. +export const app = run({ + async loadModule(moduleUrl, exportName) { + let mod = await import(moduleUrl); + return mod[exportName]; + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/app/assets.ts b/dev-packages/e2e-tests/test-applications/remix-v3/app/assets.ts new file mode 100644 index 000000000000..a3c153578aa6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-v3/app/assets.ts @@ -0,0 +1,15 @@ +import { createAssetServer } from 'remix/assets'; + +export const assets = createAssetServer({ + basePath: '/assets', + rootDir: process.cwd(), + allowFiles: ['app/routes.ts', 'app/**/public/**'], + allowPackages: ['remix', '@sentry/remix'], + minify: true, + watch: false, +}); + +const entry = 'app/actions/public/entry.ts'; + +export const entryHref = await assets.getHref(entry); +export const entryPreloads = await assets.getPreloads(entry); diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/app/router.ts b/dev-packages/e2e-tests/test-applications/remix-v3/app/router.ts new file mode 100644 index 000000000000..aef480456066 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-v3/app/router.ts @@ -0,0 +1,21 @@ +import { createRouter, type MiddlewareContext } from 'remix/router'; +import { render } from 'remix/middleware/render'; + +import controller from './actions/controller.tsx'; +import { assets } from './assets.ts'; +import { routes } from './routes.ts'; + +const renderMiddleware = render({ assets }); +type AppContext = MiddlewareContext<[typeof renderMiddleware]>; + +declare module 'remix/router' { + interface RouterTypes { + context: AppContext; + } +} + +export const router = createRouter({ + middleware: [renderMiddleware], +}); + +router.map(routes, controller); diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/app/routes.ts b/dev-packages/e2e-tests/test-applications/remix-v3/app/routes.ts new file mode 100644 index 000000000000..1ebc77927332 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-v3/app/routes.ts @@ -0,0 +1,6 @@ +import { get, route } from 'remix/routes'; + +export const routes = route({ + assets: get('/assets/*path'), + home: '/', +}); diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/package.json b/dev-packages/e2e-tests/test-applications/remix-v3/package.json new file mode 100644 index 000000000000..7f5e5928e5e4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-v3/package.json @@ -0,0 +1,28 @@ +{ + "name": "remix-v3-app", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "NODE_ENV=production node --import @sentry/remix/v3/node server.ts", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install", + "test:assert": "pnpm test" + }, + "dependencies": { + "@sentry/remix": "file:../../packed/sentry-remix-packed.tgz", + "remix": "3.0.0-rc.1" + }, + "devDependencies": { + "@playwright/test": "~1.56.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry/core": "file:../../packed/sentry-core-packed.tgz", + "@types/node": "^24.6.0", + "typescript": "~5.9.0" + }, + "volta": { + "node": "24.18.0", + "pnpm": "9.15.9" + } +} diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/remix-v3/playwright.config.mjs new file mode 100644 index 000000000000..fe438320a9c1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-v3/playwright.config.mjs @@ -0,0 +1,9 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, + port: 3060, + eventProxyPort: 3061, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/server.ts b/dev-packages/e2e-tests/test-applications/remix-v3/server.ts new file mode 100644 index 000000000000..56408af2bbe9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-v3/server.ts @@ -0,0 +1,27 @@ +import * as Sentry from '@sentry/remix/v3'; +import * as http from 'node:http'; +import { createRequestListener } from 'remix/node-fetch-server'; + +import { router } from './app/router.ts'; + +Sentry.init({ + dsn: process.env.E2E_TEST_DSN, + tunnel: 'http://localhost:3061/', + tracesSampleRate: 1.0, +}); + +const port = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 3060; + +const server = http.createServer( + createRequestListener(async request => { + try { + return await router.fetch(request); + } catch { + return new Response('Internal Server Error', { status: 500 }); + } + }), +); + +server.listen(port, () => { + console.log(`Server listening on http://localhost:${port}`); +}); diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/remix-v3/start-event-proxy.mjs new file mode 100644 index 000000000000..a1fc09b10ccc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-v3/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3061, + proxyServerName: 'remix-v3', +}); diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/tests/smoke.test.ts b/dev-packages/e2e-tests/test-applications/remix-v3/tests/smoke.test.ts new file mode 100644 index 000000000000..85e4619e49c4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-v3/tests/smoke.test.ts @@ -0,0 +1,20 @@ +import { expect, test } from '@playwright/test'; +import { waitForStreamedSpan } from '@sentry-internal/test-utils'; + +// There is no instrumentation yet. This app exists so later pull requests add instrumentation and its +// tests together, rather than also introducing a new CI surface. +test('the app boots under the Sentry --import entry', async ({ page }) => { + await page.goto('/'); + + await expect(page.locator('#home')).toBeVisible(); +}); + +test('Sentry.init from the v3 subpath reports a server span', async ({ baseURL }) => { + // Names are still URL based. This only proves the subpath resolves and the SDK is live. + const spanPromise = waitForStreamedSpan('remix-v3', span => span.is_segment === true); + + await fetch(`${baseURL}/`); + + const span = await spanPromise; + expect(span.attributes?.['http.request.method']?.value).toBe('GET'); +}); diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/tsconfig.json b/dev-packages/e2e-tests/test-applications/remix-v3/tsconfig.json new file mode 100644 index 000000000000..0c60cc275fbb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/remix-v3/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "strict": true, + "lib": ["ES2024", "DOM", "DOM.Iterable"], + "types": ["node"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ESNext", + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "skipLibCheck": true, + "jsx": "react-jsx", + "jsxImportSource": "remix/ui", + "noEmit": true + }, + "exclude": ["tests"] +}