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
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ export default [
route('error-middleware', 'routes/performance/error-middleware.tsx'),
route('lazy-route', 'routes/performance/lazy-route.tsx'),
route('fetcher-test', 'routes/performance/fetcher-test.tsx'),
route('redis', 'routes/performance/redis.tsx'),
]),
] satisfies RouteConfig;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Redis from 'ioredis';
import type { Route } from './+types/redis';

const redis = new Redis();

export async function loader() {
const key = 'cache:greeting';
await redis.set(key, 'hello from react-router');
const value = await redis.get(key);

return { value };
}

export default function RedisPage({ loaderData }: Route.ComponentProps) {
const { value } = loaderData;
return (
<div>
<h1>Redis Page</h1>
<div id="redis-value">{value}</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
redis:
image: redis:8
restart: always
container_name: e2e-tests-react-router-7-instrumentation-redis
ports:
- '6379:6379'
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 1s
timeout: 3s
retries: 30
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { execSync } from 'child_process';
import { dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));

export default async function globalSetup() {
execSync('docker compose up -d --wait', { cwd: __dirname, stdio: 'inherit' });
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"@react-router/node": "latest",
"@react-router/serve": "latest",
"@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz",
"ioredis": "^5.4.1",
"isbot": "^5.1.17",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
import { fileURLToPath } from 'url';

const config = getPlaywrightConfig({
startCommand: `PORT=3030 pnpm start`,
port: 3030,
});
const config = getPlaywrightConfig(
{
startCommand: `PORT=3030 pnpm start`,
port: 3030,
},
// Boot Redis before the tests run, outside the webServer startup-timeout window.
{ globalSetup: fileURLToPath(new URL('./global-setup.mjs', import.meta.url)) },
);

export default config;
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,31 @@ test.describe('client - instrumentation API pageload', () => {
});
});

test('parameterizes the pageload transaction for dynamic routes', async ({ page }) => {
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return (
transactionEvent.transaction === '/performance/with/:param' &&
transactionEvent.contexts?.trace?.op === 'pageload'
);
});

await page.goto(`/performance/with/some-param`);

const transaction = await txPromise;

expect(transaction).toMatchObject({
contexts: {
trace: {
op: 'pageload',
data: { 'sentry.source': 'route' },
},
},
transaction: '/performance/with/:param',
type: 'transaction',
transaction_info: { source: 'route' },
});
});

test('should link server and client transactions with same trace_id', async ({ page }) => {
const serverTxPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,20 @@ test.describe('server - instrumentation API performance', () => {
origin: 'auto.function.react_router.instrumentation_api',
});
});

test('sends exactly one http.server transaction per request (no double-instrumentation)', async ({ page }) => {
const httpServerTransactions: Array<string | undefined> = [];
void waitForTransaction(APP_NAME, async transactionEvent => {
if (transactionEvent.contexts?.trace?.op === 'http.server') {
httpServerTransactions.push(transactionEvent.transaction);
}
return false;
});

await page.goto(`/performance`);
// Give any (erroneous) duplicate transaction time to arrive before asserting.
await page.waitForTimeout(3000);

expect(httpServerTransactions).toEqual(['GET /performance']);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

test.describe('server - redis db spans (instrumentation API)', () => {
test('OTel db.redis spans nest under the native instrumentation-API http.server transaction', async ({ page }) => {
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return (
transactionEvent.transaction === 'GET /performance/redis' &&
(transactionEvent.spans?.some(span => span.op === 'db.redis') ?? false)
);
});

await page.goto('/performance/redis');

const transaction = await txPromise;

// The server transaction must come from the native instrumentation API (not the legacy handler),
// proving auto-instrumented OTel spans still share context with the React Router server span.
expect(transaction.contexts?.trace?.op).toBe('http.server');
expect(transaction.contexts?.trace?.origin).toBe('auto.http.react_router.instrumentation_api');

const redisSpans = transaction.spans!.filter(span => span.op === 'db.redis');

// loader runs SET then GET => at least two redis command spans
expect(redisSpans.length).toBeGreaterThanOrEqual(2);
expect(redisSpans.every(span => span.data?.['db.system'] === 'redis')).toBe(true);
expect(redisSpans.every(span => typeof span.parent_span_id === 'string')).toBe(true);
expect(redisSpans.some(span => span.data?.['net.peer.port'] === 6379)).toBe(true);

const statements = redisSpans.map(span => String(span.data?.['db.statement'] ?? '').toLowerCase());
Comment on lines +27 to +31

@s1gr1d s1gr1d Jun 11, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for this PR but heads-up that db.statement and db.system are deprecated (also net.*): https://getsentry.github.io/sentry-conventions/attributes/db

expect(statements.some(statement => statement.startsWith('set'))).toBe(true);
expect(statements.some(statement => statement.startsWith('get'))).toBe(true);
});
});
Loading