Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ services:
db:
image: mysql:8.0
restart: always
container_name: e2e-tests-remix-orchestrion-mysql
container_name: e2e-tests-create-remix-app-v2-mysql
# The `mysql` 2.x driver doesn't speak MySQL 8's default
# `caching_sha2_password` auth, so force the legacy plugin.
command: ['--default-authentication-plugin=mysql_native_password']
Expand All @@ -20,7 +20,7 @@ services:
redis:
image: redis:7
restart: always
container_name: e2e-tests-remix-orchestrion-redis
container_name: e2e-tests-create-remix-app-v2-redis
ports:
- '6379:6379'
healthcheck:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default async function globalSetup() {
// recognize a leftover container from a previous (e.g. interrupted) run as
// part of the same project - but the container names are fixed, so the daemon
// still refuses to create new ones. Force-remove any stale leftovers first.
for (const container of ['e2e-tests-remix-orchestrion-mysql', 'e2e-tests-remix-orchestrion-redis']) {
for (const container of ['e2e-tests-create-remix-app-v2-mysql', 'e2e-tests-create-remix-app-v2-redis']) {
try {
execSync(`docker rm -f ${container}`, { stdio: 'ignore' });
} catch {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
const Sentry = require('@sentry/remix');

const injectOrchestrion = process.env.INJECT_ORCHESTRION === 'true';

if (injectOrchestrion) {
// Opt into diagnostics-channel-based auto-instrumentation. This registers the
// channel subscribers (e.g. for mysql and ioredis) that turn the
// diagnostics-channel events - injected at build time by the orchestrion Vite
// plugin (see vite.config.ts) - into Sentry spans. Must run before Sentry.init().
Sentry.experimentalUseDiagnosticsChannelInjection();
}

Sentry.init({
tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production!
environment: 'qa', // dynamic sampling bias to keep transactions
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
{
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"build": "remix vite:build && pnpm typecheck",
"dev": "remix vite:dev",
"start": "NODE_OPTIONS='--require=./instrument.server.cjs' remix-serve build/server/index.js",
"typecheck": "tsc",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
"test:build": "pnpm install && pnpm build",
"test:assert": "pnpm playwright test"
"test:assert": "pnpm playwright test",
"test:build:orchestrion": "INJECT_ORCHESTRION=true pnpm test:build",
"test:assert:orchestrion": "INJECT_ORCHESTRION=true pnpm test:assert"
},
"dependencies": {
"@sentry/remix": "file:../../packed/sentry-remix-packed.tgz",
"@sentry/server-utils": "file:../../packed/sentry-server-utils-packed.tgz",
"@remix-run/css-bundle": "2.17.4",
"@remix-run/node": "2.17.4",
"@remix-run/react": "2.17.4",
"@remix-run/serve": "2.17.4",
"isbot": "^3.6.8",
"ioredis": "5.10.1",
"mysql": "^2.18.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
Expand All @@ -25,6 +31,7 @@
"@sentry-internal/test-utils": "link:../../../test-utils",
"@remix-run/dev": "2.17.4",
"@remix-run/eslint-config": "2.17.4",
"@types/mysql": "^2.15.26",
"@types/react": "^18.2.64",
"@types/react-dom": "^18.2.34",
"@types/prop-types": "15.7.7",
Expand All @@ -36,6 +43,15 @@
"resolutions": {
"@types/react": "18.2.22"
},
"sentryTest": {
"variants": [
{
"build-command": "pnpm test:build:orchestrion",
"assert-command": "pnpm test:assert:orchestrion",
"label": "create-remix-app-v2 (orchestrion)"
}
]
},
"volta": {
"extends": "../../package.json"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
import { fileURLToPath } from 'url';

const config = getPlaywrightConfig({
startCommand: `pnpm start`,
});
const injectOrchestrion = process.env.INJECT_ORCHESTRION === 'true';

const config = getPlaywrightConfig(
{
startCommand: `pnpm start`,
},
// The orchestrion variant exercises real MySQL/Redis. Boot them before the tests run,
// outside the webServer startup-timeout window. In the default variant no DB is needed.
injectOrchestrion
? {
globalSetup: fileURLToPath(new URL('./global-setup.mjs', import.meta.url)),
globalTeardown: fileURLToPath(new URL('./global-teardown.mjs', import.meta.url)),
}
: {},
);

export default config;

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { expect, test } from '@playwright/test';

// The `db.test.ts` runtime assertions prove orchestrion spans appear, but spans alone
// don't prove they came from the BUILD-time transform: if the Vite plugin silently
// failed to load, the deps would stay external and the runtime `--require` hook would
// inject the channels at runtime instead - the span tests would still pass. These
// assertions inspect the built server bundle directly so a broken plugin can't hide
// behind that runtime fallback. Only relevant in the orchestrion variant.
test.describe('orchestrion build-time injection', () => {
test.skip(process.env.INJECT_ORCHESTRION !== 'true', 'Only runs in the orchestrion variant');

const serverBundle = readFileSync(path.join(process.cwd(), 'build/server/index.js'), 'utf8');

test('force-bundles the instrumented deps instead of externalizing them', () => {
// The plugin adds mysql/ioredis to `ssr.noExternal` so the transform sees their
// source. Without it they'd be left as bare imports or `require(...)` calls resolved
// from node_modules at runtime - untouched, with no channels injected.
expect(serverBundle).not.toMatch(/(from\s*["']mysql["']|require\(["']mysql["']\))/);
expect(serverBundle).not.toMatch(/(from\s*["']ioredis["']|require\(["']ioredis["']\))/);
});

test('injects the diagnostics-channel publishers into the bundled deps', () => {
// The transform wraps each instrumented function with a `tracingChannel("<name>")`
// publisher whose channel name is a string literal. The subscriber side passes the
// channel name as a variable, so a literal-arg match is unique to the injected
// publisher and proves the build-time transform ran.
expect(serverBundle).toMatch(/tracingChannel(\$?\d)?\(["']orchestrion:mysql:query["']\)/);
expect(serverBundle).toMatch(/tracingChannel(\$?\d)?\(["']orchestrion:ioredis:command["']\)/);
expect(serverBundle).toMatch(/tracingChannel(\$?\d)?\(["']orchestrion:ioredis:connect["']\)/);
});

test('injects the diagnostics-channel publishers into @remix-run/server-runtime', () => {
// Remix's own instrumentation is orchestrion-based too: the transform force-bundles
// and injects channels into `@remix-run/server-runtime`
expect(serverBundle).toMatch(
/tracingChannel(\$?\d)?\(["']orchestrion:@remix-run\/server-runtime:requestHandler["']\)/,
);
expect(serverBundle).toMatch(
/tracingChannel(\$?\d)?\(["']orchestrion:@remix-run\/server-runtime:matchServerRoutes["']\)/,
);
expect(serverBundle).toMatch(
/tracingChannel(\$?\d)?\(["']orchestrion:@remix-run\/server-runtime:callRouteLoader["']\)/,
);
Comment thread
cursor[bot] marked this conversation as resolved.
expect(serverBundle).toMatch(
/tracingChannel(\$?\d)?\(["']orchestrion:@remix-run\/server-runtime:callRouteAction["']\)/,
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

// These assertions only hold in the orchestrion variant (INJECT_ORCHESTRION=true), which
// force-bundles + transforms mysql/ioredis and boots the databases via docker-compose.
test.describe('orchestrion DB instrumentation', () => {
test.skip(process.env.INJECT_ORCHESTRION !== 'true', 'Only runs in the orchestrion variant');

test('Instruments ioredis automatically via orchestrion', async ({ baseURL }) => {
const transactionEventPromise = waitForTransaction('create-remix-app-v2', transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' && !!transactionEvent.transaction?.includes('db-ioredis')
);
});

await fetch(`${baseURL}/db-ioredis`);

const transactionEvent = await transactionEventPromise;

const spans = transactionEvent.spans || [];

expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.orchestrion.redis',
description: 'set test-key [1 other arguments]',
status: 'ok',
data: expect.objectContaining({
'db.system': 'redis',
'db.statement': 'set test-key [1 other arguments]',
}),
}),
);
expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.orchestrion.redis',
description: 'get test-key',
status: 'ok',
data: expect.objectContaining({
'db.system': 'redis',
'db.statement': 'get test-key',
}),
}),
);
});

test('Instruments mysql automatically via orchestrion', async ({ baseURL }) => {
const transactionEventPromise = waitForTransaction('create-remix-app-v2', transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' && !!transactionEvent.transaction?.includes('db-mysql')
);
});

await fetch(`${baseURL}/db-mysql`);

const transactionEvent = await transactionEventPromise;

const spans = transactionEvent.spans || [];

expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.orchestrion.mysql',
description: 'SELECT 1 + 1 AS solution',
status: 'ok',
data: expect.objectContaining({
'db.system': 'mysql',
'db.statement': 'SELECT 1 + 1 AS solution',
'db.user': 'root',
'db.connection_string': expect.any(String),
'net.peer.name': expect.any(String),
'net.peer.port': 3306,
}),
}),
);
expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.orchestrion.mysql',
description: 'SELECT NOW()',
status: 'ok',
data: expect.objectContaining({
'db.system': 'mysql',
'db.statement': 'SELECT NOW()',
'db.user': 'root',
'db.connection_string': expect.any(String),
'net.peer.name': expect.any(String),
'net.peer.port': 3306,
}),
}),
);
});
});
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { vitePlugin as remix } from '@remix-run/dev';
import { sentryRemixVitePlugin } from '@sentry/remix';
import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/vite';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

const injectOrchestrion = process.env.INJECT_ORCHESTRION === 'true';

export default defineConfig({
plugins: [
remix({
ignoredRouteFiles: ['**/.*'],
serverModuleFormat: 'cjs',
Comment thread
cursor[bot] marked this conversation as resolved.
}),
sentryRemixVitePlugin(),
// In the orchestrion variant, run the orchestrion code transform over the SSR
// server bundle and force-bundle the instrumented deps (mysql, ioredis,
// @remix-run/server-runtime, …) so their diagnostics-channel calls are injected
// at build time.
...(injectOrchestrion ? [sentryOrchestrionPlugin()] : []),
tsconfigPaths(),
],
});

This file was deleted.

This file was deleted.

Loading
Loading