Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott

## 11.4.0

- feat(sveltekit): Support stable SvelteKit 3 ([#25009](https://github.com/getsentry/sentry-javascript/pull/25009))
- fix(remix): Match the Remix 3.0.0 package versions ([#25008](https://github.com/getsentry/sentry-javascript/pull/25008))

## 11.3.0

### Important Changes
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Record<string, never>>) {
Expand All @@ -10,10 +11,11 @@ function HomePage(handle: Handle<Record<string, never>>) {
<head>
<meta charSet="utf-8" />
<title>Sentry Remix 3</title>
{entryPreloads.map(href => (
<ImportMap value={entry.importMap} />
{entry.preloads.map(href => (
<link key={href} rel="modulepreload" href={href} />
))}
<script type="module" src={entryHref}></script>
<script type="module" src={entry.href}></script>
</head>
<body>
<h1 id="home">Sentry Remix 3</h1>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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') {
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"isolatedModules": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"jsxImportSource": "remix/ui",
"jsxImportSource": "remix/component",
"noEmit": true
},
"exclude": ["tests"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,17 @@
"devDependencies": {
"@playwright/test": "~1.63.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sveltejs/adapter-cloudflare": "next",
"@sveltejs/kit": "next",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@sveltejs/adapter-cloudflare": "^8.0.0",
"@sveltejs/kit": "^3.0.0",
"@sveltejs/vite-plugin-svelte": "^7.3.1",
"cookie": "^2.0.1",
"svelte": "^5.48.0",
"svelte": "^5.57.1",
"typescript": "^6.0.0",
"vite": "^8.0.0",
"vite": "^8.0.12",
"wrangler": "^4.118.0"
},
"volta": {
"node": "22.20.0",
"extends": "../../package.json"
},
"sentryTest": {
"optional": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,17 @@
"devDependencies": {
"@playwright/test": "~1.63.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sveltejs/adapter-auto": "next",
"@sveltejs/adapter-node": "next",
"@sveltejs/kit": "next",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"svelte": "^5.48.0",
"@sveltejs/adapter-node": "^6.0.0",
"@sveltejs/kit": "^3.0.0",
"@sveltejs/vite-plugin-svelte": "^7.3.1",
"svelte": "^5.57.1",
"svelte-check": "^4.6.0",
"typescript": "^6.0.0",
"vite": "^8.0.0"
"vite": "^8.0.12"
},
"volta": {
"node": "22.20.0",
"extends": "../../package.json"
},
"sentryTest": {
"optional": true
},
"type": "module"
}
6 changes: 3 additions & 3 deletions packages/remix/src/v3/client/browserTracingIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type Options = Parameters<typeof originalBrowserTracingIntegration>[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 {
Expand Down Expand Up @@ -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: {
Expand All @@ -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.
*/
Expand Down
30 changes: 16 additions & 14 deletions packages/remix/src/v3/client/errors.ts
Original file line number Diff line number Diff line change
@@ -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<object>();

Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion packages/remix/src/v3/client/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()];
Expand Down
25 changes: 14 additions & 11 deletions packages/remix/test/v3/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
18 changes: 12 additions & 6 deletions packages/server-utils/src/orchestrion/config/remix-v3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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' },
Expand All @@ -23,31 +23,37 @@ 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.
{
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 = {
REMIX_V3_CREATE_ROUTER: 'orchestrion:@remix-run/fetch-router:createRouter',
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;
23 changes: 18 additions & 5 deletions packages/server-utils/test/orchestrion/load-hook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));

Expand All @@ -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 });
Expand Down
2 changes: 1 addition & 1 deletion packages/sveltekit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"access": "public"
},
"peerDependencies": {
"@sveltejs/kit": "2.x || ^3.0.0-0",
"@sveltejs/kit": "2.x || 3.x",
"vite": "*"
},
"peerDependenciesMeta": {
Expand Down
Loading