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 index 9d52a907bcbe..69be89b5d961 100644 --- 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 @@ -1,7 +1,8 @@ import { createController } from 'remix/router'; -import type { Handle } from 'remix/ui'; +import type { Handle } from 'remix/component'; +import { ImportMap } from 'remix/component/server'; -import { assets, entryHref, entryPreloads } from '../assets.ts'; +import { assets, entry } from '../assets.ts'; import { routes } from '../routes.ts'; function HomePage(handle: Handle>) { @@ -10,10 +11,11 @@ function HomePage(handle: Handle>) { Sentry Remix 3 - {entryPreloads.map(href => ( + + {entry.preloads.map(href => ( ))} - +

Sentry Remix 3

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 index c18e0f0a8967..8a3876dc890b 100644 --- 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 @@ -1,5 +1,5 @@ import * as Sentry from '@sentry/remix/v3/client'; -import { createElement, run } from 'remix/ui'; +import { createElement, run } from 'remix/component'; import { throwError } from './throw-error.ts'; 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 index 3cb55a9ab90d..345e6b2eaffd 100644 --- a/dev-packages/e2e-tests/test-applications/remix-v3/app/assets.ts +++ b/dev-packages/e2e-tests/test-applications/remix-v3/app/assets.ts @@ -19,7 +19,5 @@ export const assets = createAssetServer({ }, }); -const entry = 'app/actions/public/entry.ts'; - -export const entryHref = await assets.getHref(entry); -export const entryPreloads = await assets.getPreloads(entry); +// Served modules keep their bare imports, so the document must render this entry's import map. +export const entry = await assets.getScriptEntry('app/actions/public/entry.ts'); diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/package.json b/dev-packages/e2e-tests/test-applications/remix-v3/package.json index 7f5e5928e5e4..5cb015c44c9a 100644 --- a/dev-packages/e2e-tests/test-applications/remix-v3/package.json +++ b/dev-packages/e2e-tests/test-applications/remix-v3/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@sentry/remix": "file:../../packed/sentry-remix-packed.tgz", - "remix": "3.0.0-rc.1" + "remix": "3.0.0" }, "devDependencies": { "@playwright/test": "~1.56.0", diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/tests/browser-transform.test.ts b/dev-packages/e2e-tests/test-applications/remix-v3/tests/browser-transform.test.ts index 08b73e2ceaad..e5f00ec5696e 100644 --- a/dev-packages/e2e-tests/test-applications/remix-v3/tests/browser-transform.test.ts +++ b/dev-packages/e2e-tests/test-applications/remix-v3/tests/browser-transform.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -test('the served remix/ui runtime is instrumented', async ({ page, baseURL }) => { +test('the served remix/component runtime is instrumented', async ({ page, baseURL }) => { const served: string[] = []; page.on('response', response => { if (response.request().resourceType() === 'script') { @@ -10,7 +10,7 @@ test('the served remix/ui runtime is instrumented', async ({ page, baseURL }) => await page.goto('/', { waitUntil: 'load' }); - const runModule = served.find(url => /@remix-run\/ui\/dist\/runtime\/run\.js/.test(decodeURIComponent(url))); + const runModule = served.find(url => /@remix-run\/component\/dist\/runtime\/run\.js/.test(decodeURIComponent(url))); expect(runModule, 'run.js was not served').toBeDefined(); const code = await (await fetch(runModule as string)).text(); diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/tests/client.test.ts b/dev-packages/e2e-tests/test-applications/remix-v3/tests/client.test.ts index 79d36cf682c0..9428d8b53565 100644 --- a/dev-packages/e2e-tests/test-applications/remix-v3/tests/client.test.ts +++ b/dev-packages/e2e-tests/test-applications/remix-v3/tests/client.test.ts @@ -18,7 +18,7 @@ test('sends a pageload span', async ({ page }) => { test('sends a navigation span for a link the runtime intercepts', async ({ page }) => { await page.goto('/'); - // Selected by origin, not just op. `remix/ui` never touches History, so a span from the upstream + // Selected by origin, not just op. `remix/component` never touches History, so a span from the upstream // handler would mean this SDK did not produce it. const spanPromise = waitForStreamedSpan( APP_NAME, diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/tsconfig.json b/dev-packages/e2e-tests/test-applications/remix-v3/tsconfig.json index 0c60cc275fbb..443890d69e16 100644 --- a/dev-packages/e2e-tests/test-applications/remix-v3/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/remix-v3/tsconfig.json @@ -12,7 +12,7 @@ "isolatedModules": true, "skipLibCheck": true, "jsx": "react-jsx", - "jsxImportSource": "remix/ui", + "jsxImportSource": "remix/component", "noEmit": true }, "exclude": ["tests"] diff --git a/packages/remix/src/v3/client/browserTracingIntegration.ts b/packages/remix/src/v3/client/browserTracingIntegration.ts index c9832db5c4cd..38cd64183fb0 100644 --- a/packages/remix/src/v3/client/browserTracingIntegration.ts +++ b/packages/remix/src/v3/client/browserTracingIntegration.ts @@ -18,7 +18,7 @@ type Options = Parameters[0]; * Browser tracing for Remix 3. * * Page loads stay with the upstream integration, because they are ordinary document loads. Navigations - * do not: `remix/ui` intercepts links and form submissions through the Navigation API and never touches + * do not: `remix/component` intercepts links and form submissions through the Navigation API and never touches * History, so the upstream handler never fires. */ export function browserTracingIntegration(options: Options = {}): Integration { @@ -52,7 +52,7 @@ function instrumentNavigationApi(client: Client): void { startBrowserTracingNavigationSpan( client, { - // Remix 3 gives the browser no route to name this after: `remix/ui` exposes no matched route + // Remix 3 gives the browser no route to name this after: `remix/component` exposes no matched route // and never matches client side. Passing the server's pattern down is tracked in (#24872). name: hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : pathnameOf(url) || '/', attributes: { @@ -70,7 +70,7 @@ function instrumentNavigationApi(client: Client): void { /** * Whether the runtime will keep this navigation inside the current document. * - * The same three conditions `startNavigationListener` in `@remix-run/ui` checks before intercepting. + * The same three conditions `startNavigationListener` in `@remix-run/component` checks before intercepting. * What it declines becomes a new document load, which already gets a page load span, so a navigation * span here would count the same click twice. */ diff --git a/packages/remix/src/v3/client/errors.ts b/packages/remix/src/v3/client/errors.ts index fd636cacba27..000501c78c92 100644 --- a/packages/remix/src/v3/client/errors.ts +++ b/packages/remix/src/v3/client/errors.ts @@ -1,18 +1,18 @@ import { captureException } from '@sentry/browser'; import { tracingChannel } from './diagnosticsChannelShim'; -/** The `AppRuntime` returned by `run()` from `remix/ui`, an EventTarget emitting `error`. */ +/** The `AppRuntime` returned by `run()` from `remix/component`, an EventTarget emitting `error`. */ export interface AppRuntimeLike { addEventListener(type: 'error', listener: (event: Event) => void): void; } const MECHANISM_TYPE = 'auto.ui.remix_v3'; -// The channel orchestrion will inject into `@remix-run/ui`'s `run()` once the asset server transforms -// browser modules. Nothing publishes to it yet, so the subscription below is dormant. The name is -// written out because the channel names live in `@sentry/server-utils`, which must not reach the -// browser. -const UI_RUN_CHANNEL = 'orchestrion:@remix-run/ui:run'; +// The channels orchestrion injects into `run()` once the asset server transforms browser modules. +// `@remix-run/ui` is the release candidate name, `@remix-run/component` the stable one; an app loads +// only one. The names are written out because the channel names live in `@sentry/server-utils`, which +// must not reach the browser. +const RUN_CHANNELS = ['orchestrion:@remix-run/ui:run', 'orchestrion:@remix-run/component:run']; const attached = new WeakSet(); @@ -53,14 +53,16 @@ export function instrumentClientRuntime(): void { } subscribed = true; - tracingChannel(UI_RUN_CHANNEL).subscribe({ - end(context) { - const app = (context as { result?: unknown }).result; - if (isAppRuntime(app)) { - captureRuntimeErrors(app); - } - }, - }); + for (const name of RUN_CHANNELS) { + tracingChannel(name).subscribe({ + end(context) { + const app = (context as { result?: unknown }).result; + if (isAppRuntime(app)) { + captureRuntimeErrors(app); + } + }, + }); + } } function isAppRuntime(value: unknown): value is AppRuntimeLike { diff --git a/packages/remix/src/v3/client/sdk.ts b/packages/remix/src/v3/client/sdk.ts index cbf5560c1296..998bfc627a35 100644 --- a/packages/remix/src/v3/client/sdk.ts +++ b/packages/remix/src/v3/client/sdk.ts @@ -10,7 +10,7 @@ import { instrumentClientRuntime } from './errors'; * * Browser tracing is added here rather than left to the app, because the Navigation API variant is the * only one that reports anything in Remix 3. Everything else is plain `@sentry/browser`. Nothing from - * `@sentry/react` applies: `remix/ui` is its own runtime, with no React and no reconciler to hook. + * `@sentry/react` applies: `remix/component` is its own runtime, with no React and no reconciler to hook. */ export function getDefaultIntegrations(options: BrowserOptions): Integration[] { return [...getBrowserDefaultIntegrations(options), browserTracingIntegration()]; diff --git a/packages/remix/test/v3/errors.test.ts b/packages/remix/test/v3/errors.test.ts index c5224d7fd77e..92b2b115af96 100644 --- a/packages/remix/test/v3/errors.test.ts +++ b/packages/remix/test/v3/errors.test.ts @@ -25,19 +25,22 @@ describe('captureRuntimeErrors', () => { instrumentClientRuntime(); }); - it('reports one error per dispatch, however often instrumentation was set up', () => { - captureException.mockClear(); - const app = fakeApp(); - tracingChannel('orchestrion:@remix-run/ui:run').end.publish({ result: app }); + it.each(['orchestrion:@remix-run/ui:run', 'orchestrion:@remix-run/component:run'])( + 'reports one error per dispatch on %s, however often instrumentation was set up', + channelName => { + captureException.mockClear(); + const app = fakeApp(); + tracingChannel(channelName).end.publish({ result: app }); - const error = new Error('render failed'); - app.dispatchEvent(errorEvent(error)); + const error = new Error('render failed'); + app.dispatchEvent(errorEvent(error)); - expect(captureException).toHaveBeenCalledTimes(1); - expect(captureException).toHaveBeenCalledWith(error, { - mechanism: { handled: false, type: 'auto.ui.remix_v3' }, - }); - }); + expect(captureException).toHaveBeenCalledTimes(1); + expect(captureException).toHaveBeenCalledWith(error, { + mechanism: { handled: false, type: 'auto.ui.remix_v3' }, + }); + }, + ); it('attaches only once to the same app', () => { captureException.mockClear(); diff --git a/packages/server-utils/src/orchestrion/config/remix-v3.ts b/packages/server-utils/src/orchestrion/config/remix-v3.ts index 8dfc20daf5b5..3baec058a966 100644 --- a/packages/server-utils/src/orchestrion/config/remix-v3.ts +++ b/packages/server-utils/src/orchestrion/config/remix-v3.ts @@ -2,7 +2,8 @@ import type { InstrumentationConfig } from '../apmTypes'; // Remix 3 is a ground up rewrite sharing no modules with Remix 2, so it gets its own config rather // than a widened `versionRange` on `./remix.ts`. The two never collide: this matches the -// `@remix-run/*` 0.x packages, that one `@remix-run/server-runtime`. +// `@remix-run/*` packages, that one `@remix-run/server-runtime`. Ranges start at the release +// candidates and end before 2.0.0, because the packages went 0.x to 1.0.0 with Remix 3.0.0. // // `remix/router` is a one line `export * from '@remix-run/fetch-router'`, so matching the real module // covers both import styles. @@ -13,8 +14,7 @@ export const remixV3Config: InstrumentationConfig[] = [ channelName: 'createRouter', module: { name: '@remix-run/fetch-router', - // Still 0.x during the Remix 3 release candidate, so the range is deliberately narrow. - versionRange: '>=0.21.0 <1', + versionRange: '>=0.21.0 <2', filePath: 'dist/lib/router.js', }, functionQuery: { functionName: 'createRouter', kind: 'Sync' }, @@ -23,7 +23,7 @@ export const remixV3Config: InstrumentationConfig[] = [ // returns, so browser modules carry debug IDs without any config from the app. { channelName: 'createAssetServer', - module: { name: '@remix-run/assets', versionRange: '>=0.6.0 <1', filePath: 'dist/lib/asset-server.js' }, + module: { name: '@remix-run/assets', versionRange: '>=0.6.0 <2', filePath: 'dist/lib/asset-server.js' }, functionQuery: { functionName: 'createAssetServer', kind: 'Sync' }, }, // The only error hook that covers an app whose fetch handler is not a router. @@ -31,18 +31,23 @@ export const remixV3Config: InstrumentationConfig[] = [ channelName: 'createRequestListener', module: { name: '@remix-run/node-fetch-server', - versionRange: '>=0.14.0 <1', + versionRange: '>=0.14.0 <2', filePath: 'dist/lib/request-listener.js', }, functionQuery: { functionName: 'createRequestListener', kind: 'Sync' }, }, // Browser side, applied by the asset server's loader chain. The only way to see component render - // errors without application code. + // errors without application code. `@remix-run/ui` was renamed to `@remix-run/component` in 1.0.0. { channelName: 'run', module: { name: '@remix-run/ui', versionRange: '>=0.8.0 <1', filePath: 'dist/runtime/run.js' }, functionQuery: { functionName: 'run', kind: 'Sync' }, }, + { + channelName: 'run', + module: { name: '@remix-run/component', versionRange: '>=1.0.0 <2', filePath: 'dist/runtime/run.js' }, + functionQuery: { functionName: 'run', kind: 'Sync' }, + }, ]; export const remixV3Channels = { @@ -50,4 +55,5 @@ export const remixV3Channels = { REMIX_V3_CREATE_ASSET_SERVER: 'orchestrion:@remix-run/assets:createAssetServer', REMIX_V3_CREATE_REQUEST_LISTENER: 'orchestrion:@remix-run/node-fetch-server:createRequestListener', REMIX_V3_UI_RUN: 'orchestrion:@remix-run/ui:run', + REMIX_V3_COMPONENT_RUN: 'orchestrion:@remix-run/component:run', } as const; diff --git a/packages/server-utils/test/orchestrion/load-hook.test.ts b/packages/server-utils/test/orchestrion/load-hook.test.ts index 6a4d9af73ba6..e90a51cba250 100644 --- a/packages/server-utils/test/orchestrion/load-hook.test.ts +++ b/packages/server-utils/test/orchestrion/load-hook.test.ts @@ -8,12 +8,19 @@ import { createLoadHookTransform } from '../../src/orchestrion/bundler/load-hook // The transform reads the package name and version from disk, so the fixture is a real, minimal // install layout in a temp directory. const root = fs.mkdtempSync(path.join(os.tmpdir(), 'load-hook-')); -const pkgDir = path.join(root, 'node_modules', '@remix-run', 'ui'); -fs.mkdirSync(path.join(pkgDir, 'dist', 'runtime'), { recursive: true }); -fs.writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify({ name: '@remix-run/ui', version: '0.11.0' })); -const runFile = path.join(pkgDir, 'dist', 'runtime', 'run.js'); const source = 'export function run(init) {\n return init;\n}\n'; -fs.writeFileSync(runFile, source); + +function installRunModule(name: string, version: string): string { + const pkgDir = path.join(root, 'node_modules', ...name.split('/')); + fs.mkdirSync(path.join(pkgDir, 'dist', 'runtime'), { recursive: true }); + fs.writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify({ name, version })); + const file = path.join(pkgDir, 'dist', 'runtime', 'run.js'); + fs.writeFileSync(file, source); + return file; +} + +const runFile = installRunModule('@remix-run/ui', '0.11.0'); +const componentRunFile = installRunModule('@remix-run/component', '1.0.0'); afterAll(() => fs.rmSync(root, { recursive: true, force: true })); @@ -29,6 +36,12 @@ describe('createLoadHookTransform', () => { expect(code).not.toMatch(/\brequire\(/); }); + it('instruments the renamed @remix-run/component 1.x package', () => { + const code = transform(componentRunFile, source); + + expect(code).toContain('orchestrion:@remix-run/component:run'); + }); + it('leaves a file that is not in an instrumented package alone', () => { const other = path.join(root, 'node_modules', 'left-pad', 'index.js'); fs.mkdirSync(path.dirname(other), { recursive: true });