-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
feat(remix): Run the orchestrion transform on Remix 3 browser modules #24894
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
Merged
chargome
merged 1 commit into
develop
from
charlygomez/js-3772-run-the-orchestrion-transform-on-browser-modules
Oct 2, 2026
+414
−17
Merged
Changes from all commits
Commits
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
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
20 changes: 20 additions & 0 deletions
20
dev-packages/e2e-tests/test-applications/remix-v3/tests/browser-transform.test.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,20 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
|
|
||
| test('the served remix/ui runtime is instrumented', async ({ page, baseURL }) => { | ||
| const served: string[] = []; | ||
| page.on('response', response => { | ||
| if (response.request().resourceType() === 'script') { | ||
| served.push(response.url()); | ||
| } | ||
| }); | ||
|
|
||
| await page.goto('/', { waitUntil: 'load' }); | ||
|
|
||
| const runModule = served.find(url => /@remix-run\/ui\/dist\/runtime\/run\.js/.test(decodeURIComponent(url))); | ||
| expect(runModule, 'run.js was not served').toBeDefined(); | ||
|
|
||
| const code = await (await fetch(runModule as string)).text(); | ||
| expect(code).toMatch(/diagnosticsChannelShim\.js/); | ||
| // A CommonJS `require` here would mean the transform emitted the wrong module type. | ||
| expect(code).not.toMatch(/\brequire\(/); | ||
| }); |
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
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
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 |
|---|---|---|
|
|
@@ -75,7 +75,15 @@ function createChannel(): Channel { | |
| }; | ||
| } | ||
|
|
||
| const registry = new Map<string, BrowserTracingChannel>(); | ||
| declare global { | ||
| // `var`, because only a `var` here becomes a property of `globalThis`. | ||
| // oxlint-disable-next-line no-var | ||
| var __SENTRY_REMIX_DC_REGISTRY__: Map<string, BrowserTracingChannel> | undefined; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. q/l: You think it would make sense to add this to our
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not really as this only affects remix 🤔 |
||
| } | ||
|
|
||
| // On `globalThis`, so a second copy of this module loaded under another URL shares the subscribers | ||
| // instead of splitting them. Keyed by the channel names the transform compiles in, so it stays small. | ||
| const registry = (globalThis.__SENTRY_REMIX_DC_REGISTRY__ ??= new Map<string, BrowserTracingChannel>()); | ||
|
|
||
| /** Create (or look up) a tracing channel by name. */ | ||
| export function tracingChannel(name: string): BrowserTracingChannel { | ||
|
|
||
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,114 @@ | ||
| import * as path from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
| import { createLoadHookTransform } from '@sentry/server-utils/orchestrion/load-hook'; | ||
| import { remixV3Config } from '@sentry/server-utils/orchestrion/config'; | ||
|
|
||
| /** Node's synchronous `load` hook shape, which `@remix-run/assets` reuses for `scripts.loaders`. */ | ||
| export interface ModuleLoadContext { | ||
| moduleUrl?: string; | ||
| [key: string]: unknown; | ||
| } | ||
|
|
||
| export interface ModuleLoadResult { | ||
| format: string | null | undefined; | ||
| shortCircuit?: boolean; | ||
| source?: string | ArrayBuffer | ArrayBufferView; | ||
| } | ||
|
|
||
| export type ModuleLoader = ( | ||
| url: string, | ||
| context: ModuleLoadContext, | ||
| nextLoad: (url: string, context?: Partial<ModuleLoadContext>) => ModuleLoadResult, | ||
| ) => ModuleLoadResult; | ||
|
|
||
| /** | ||
| * An asset server loader that applies orchestrion's transform to browser modules. Remix 3 has no | ||
| * bundler, so the asset server's loader chain is the only compile time hook there is. | ||
| * | ||
| * `dcModuleUrl`, the shim's public URL, must also be in `scripts.external`, so the compiler leaves | ||
| * the injected import alone. | ||
| */ | ||
| export function orchestrionLoader(dcModuleUrl: string): ModuleLoader { | ||
| const cached = loaders.get(dcModuleUrl); | ||
| if (cached) { | ||
| return cached; | ||
| } | ||
|
|
||
| // Only the Remix 3 configs. The others target server packages, and their injected snippet imports | ||
| // `@sentry/server-utils`, which cannot resolve in a browser. | ||
| const transform = createLoadHookTransform({ dcModule: dcModuleUrl, instrumentations: remixV3Config }); | ||
|
|
||
| const loader: ModuleLoader = (url, context, nextLoad) => { | ||
| const result = nextLoad(url, context); | ||
| if (result.format !== 'module' || typeof result.source !== 'string') { | ||
| return result; | ||
| } | ||
|
|
||
| // An uninstrumented module is better than one the asset server cannot serve at all. | ||
| try { | ||
| const transformed = transform(fileURLToPath(url), result.source); | ||
| return transformed === undefined ? result : { ...result, source: transformed }; | ||
| } catch { | ||
| return result; | ||
| } | ||
| }; | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| loaders.set(dcModuleUrl, loader); | ||
| return loader; | ||
| } | ||
|
|
||
| // One loader per shim URL, so options that pass through `withDebugIdOptions` twice keep one copy. | ||
| const loaders = new Map<string, ModuleLoader>(); | ||
|
|
||
| export function isOrchestrionLoader(loader: unknown): boolean { | ||
| for (const known of loaders.values()) { | ||
| if (known === loader) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Public URL of the shipped shim under the asset server's `node_modules` mount, encoded per segment as | ||
| * the asset server encodes imports. `@sentry` and `%40sentry` are different modules to a browser; a | ||
| * mismatch loads two shims and instrumentation quietly does nothing. | ||
| */ | ||
| export function getShimUrl( | ||
| basePath: string, | ||
| rootDir: string, | ||
| mounts: Readonly<Record<string, string>> | undefined, | ||
| shimPath: string, | ||
| ): string | undefined { | ||
| const nodeModulesRoot = path.join(rootDir, 'node_modules'); | ||
| const relativePath = path.relative(nodeModulesRoot, shimPath); | ||
| // Outside the mount, as with a `link:` install or another Windows drive, the asset server cannot | ||
| // serve it. Checked before splitting: a drive prefix is only absolute as part of the whole path. | ||
| if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { | ||
| return undefined; | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
| const encoded = relativePath | ||
| .split(path.sep) | ||
| .map(segment => encodeURIComponent(segment)) | ||
| .join('/'); | ||
| return `${basePath.replace(/\/$/, '')}/${npmMount(mounts)}/${encoded}`; | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| // The default mounts are `{ app: 'app', npm: 'node_modules' }`, but an app can rename them. | ||
| function npmMount(mounts: Readonly<Record<string, string>> | undefined): string { | ||
| const entry = mounts && Object.entries(mounts).find(([, dir]) => dir === 'node_modules' || dir === './node_modules'); | ||
| return entry ? entry[0] : 'npm'; | ||
| } | ||
|
|
||
| // The shim is built next to this module, so its path follows from this module's own under any | ||
| // install layout. A package self reference would not work: the `./v3` subpaths are import only. | ||
| export function resolveShimPath(): string { | ||
| let here: string; | ||
| /*! rollup-include-cjs-only */ | ||
| here = __filename; | ||
| /*! rollup-include-cjs-only-end */ | ||
| /*! rollup-include-esm-only */ | ||
| here = fileURLToPath(import.meta.url); | ||
| /*! rollup-include-esm-only-end */ | ||
| return path.join(path.dirname(here), 'client', 'diagnosticsChannelShim.js'); | ||
| } | ||
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
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
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,18 @@ | ||
| import { describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| describe('diagnosticsChannelShim', () => { | ||
| it('shares one registry between two loaded copies of the module', async () => { | ||
| const first = await import('../../src/v3/client/diagnosticsChannelShim'); | ||
| const seen: unknown[] = []; | ||
| first.tracingChannel('orchestrion:test:run').subscribe({ end: context => void seen.push(context) }); | ||
|
|
||
| // A second module instance, as a browser gets when the same file is loaded under two URLs. | ||
| vi.resetModules(); | ||
| const second = await import('../../src/v3/client/diagnosticsChannelShim'); | ||
| expect(second.tracingChannel).not.toBe(first.tracingChannel); | ||
|
|
||
| second.tracingChannel('orchestrion:test:run').end.publish({ result: 1 }); | ||
|
|
||
| expect(seen).toEqual([{ result: 1 }]); | ||
| }); | ||
| }); |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.