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 numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,6 @@ import { HydratedRouter } from 'react-router/dom';
const tracing = Sentry.reactRouterTracingIntegration({ useInstrumentationAPI: true });

Sentry.init({
traceLifecycle: 'static',
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: 'https://username@domain/123',
tunnel: `http://localhost:3031/`, // proxy server
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
import * as Sentry from '@sentry/react-router';

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://username@domain/123',
environment: 'qa', // dynamic sampling bias to keep transactions
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

test.describe('server - instrumentation API error capture', () => {
Expand All@@ -8,15 +8,15 @@ test.describe('server - instrumentation API error capture', () => {
return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-loader' && span.is_segment;
});

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

// Verify the error was captured with correct mechanism and transaction name
expect(error).toMatchObject({
Expand All@@ -36,58 +36,51 @@ test.describe('server - instrumentation API error capture', () => {
});

// Verify the transaction was also created with correct attributes
expect(transaction).toMatchObject({
transaction: 'GET /performance/error-loader',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('GET /performance/error-loader');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});

test('should include loader span in transaction even when loader throws', async ({ page }) => {
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
});
test('should include loader span in the segment even when loader throws', async ({ page }) => {
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/error-loader' && span.is_segment),
);

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const transaction = await txPromise;
const spans = await spansPromise;

// Find the loader span
const loaderSpan = transaction?.spans?.find(span => span.data?.['code.function.name'] === 'loader');
const loaderSpan = spans.find(span => span.attributes['code.function.name']?.value === 'loader');

expect(loaderSpan).toMatchObject({
data: {
'sentry.origin': 'auto.function.react_router.instrumentation_api',
'sentry.op': 'function',
'code.function.name': 'loader',
},
op: 'function',
expect(loaderSpan).toBeDefined();
expect(getSpanOp(loaderSpan!)).toBe('function');
expect(loaderSpan!.attributes).toMatchObject({
'sentry.origin': { value: 'auto.function.react_router.instrumentation_api', type: 'string' },
'sentry.op': { value: 'function', type: 'string' },
'code.function.name': { value: 'loader', type: 'string' },
});
});

test('error and transaction should share the same trace', async ({ page }) => {
test('error and segment span should share the same trace', async ({ page }) => {
const errorPromise = waitForError(APP_NAME, async errorEvent => {
return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-loader' && span.is_segment;
});

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

// Error and transaction should have the same trace_id
expect(error.contexts?.trace?.trace_id).toBe(transaction.contexts?.trace?.trace_id);
// Error and segment span should have the same trace_id
expect(error.contexts?.trace?.trace_id).toBe(span.trace_id);
});

// Skipped in dev: the action error is sometimes captured via the client instrumentation path
Expand All@@ -101,14 +94,14 @@ test.describe('server - instrumentation API error capture', () => {
return errorEvent.exception?.values?.[0]?.value === 'Action error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'POST /performance/error-action';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'POST /performance/error-action' && span.is_segment;
});

await page.goto(`/performance/error-action`);
await page.getByRole('button', { name: 'Trigger Error' }).click();

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

expect(error).toMatchObject({
exception: {
Expand All@@ -126,31 +119,25 @@ test.describe('server - instrumentation API error capture', () => {
transaction: 'POST /performance/error-action',
});

expect(transaction).toMatchObject({
transaction: 'POST /performance/error-action',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('POST /performance/error-action');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});

test('should capture middleware errors with instrumentation API mechanism', async ({ page }) => {
const errorPromise = waitForError(APP_NAME, async errorEvent => {
return errorEvent.exception?.values?.[0]?.value === 'Middleware error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-middleware';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-middleware' && span.is_segment;
});

await page.goto(`/performance/error-middleware`).catch(() => {
// Expected to fail due to middleware error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

expect(error).toMatchObject({
exception: {
Expand All@@ -168,14 +155,8 @@ test.describe('server - instrumentation API error capture', () => {
transaction: 'GET /performance/error-middleware',
});

expect(transaction).toMatchObject({
transaction: 'GET /performance/error-middleware',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('GET /performance/error-middleware');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});
});
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,94 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

// Same spans in both runs, from two injectors: the build-time transform in the server bundle, and
// the runtime hook in `react-router dev`, where the drivers stay on Node's own loader.
test.describe('server - orchestrion db instrumentation', () => {
test('instruments ioredis automatically via orchestrion', async ({ page }) => {
const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /performance/db-ioredis'
);
});
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/db-ioredis' && span.is_segment),
);

await page.goto('/performance/db-ioredis');

const transactionEvent = await transactionEventPromise;
const spans = transactionEvent.spans || [];
const spans = await spansPromise;
const segmentSpan = spans.find(span => span.name === 'GET /performance/db-ioredis' && span.is_segment)!;

// The server transaction must come from the native instrumentation API (not the legacy handler),
// The server segment must come from the native instrumentation API (not the legacy handler),
// proving the orchestrion-injected db spans share context with the React Router server span.
expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.react_router.instrumentation_api');
expect(getSpanOp(segmentSpan)).toBe('http.server');
expect(segmentSpan.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');

expect(spans).toContainEqual(
const childSpans = spans.filter(span => !span.is_segment);

expect(childSpans).toContainEqual(
expect.objectContaining({
op: 'db.query',
origin: 'auto.db.redis',
description: 'set test-key [1 other arguments]',
name: 'set test-key [1 other arguments]',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'redis',
'db.operation.name': 'set',
'db.query.text': 'set test-key [1 other arguments]',
attributes: expect.objectContaining({
'sentry.op': { value: 'db.query', type: 'string' },
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
'db.system.name': { value: 'redis', type: 'string' },
'db.operation.name': { value: 'set', type: 'string' },
'db.query.text': { value: 'set test-key [1 other arguments]', type: 'string' },
}),
}),
);
expect(spans).toContainEqual(
expect(childSpans).toContainEqual(
expect.objectContaining({
op: 'db.query',
origin: 'auto.db.redis',
description: 'get test-key',
name: 'get test-key',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'redis',
'db.operation.name': 'get',
'db.query.text': 'get test-key',
attributes: expect.objectContaining({
'sentry.op': { value: 'db.query', type: 'string' },
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
'db.system.name': { value: 'redis', type: 'string' },
'db.operation.name': { value: 'get', type: 'string' },
'db.query.text': { value: 'get test-key', type: 'string' },
}),
}),
);

// Each command maps to exactly one span (no offline-queue duplicate).
const setSpans = spans.filter(span => span.description === 'set test-key [1 other arguments]');
const setSpans = spans.filter(span => span.name === 'set test-key [1 other arguments]');
expect(setSpans).toHaveLength(1);

// Every db span nests under the native instrumentation-API http.server transaction.
const rootSpanId = transactionEvent.contexts?.trace?.span_id;
const spanIds = new Set([rootSpanId, ...spans.map(span => span.span_id)]);
const dbSpans = spans.filter(span => span.origin === 'auto.db.redis');
// Every db span nests under the native instrumentation-API http.server segment.
const spanIds = new Set(spans.filter(span => span.trace_id === segmentSpan.trace_id).map(span => span.span_id));
const dbSpans = spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.db.redis');
expect(dbSpans.every(span => typeof span.parent_span_id === 'string' && spanIds.has(span.parent_span_id))).toBe(
true,
);
});

// Under span streaming the mysql span name is the query summary, so both queries below are named
// `SELECT`. `db.query.text` is what tells them apart.
test('instruments mysql automatically via orchestrion', async ({ page }) => {
const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /performance/db-mysql'
);
});
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/db-mysql' && span.is_segment),
);

await page.goto('/performance/db-mysql');

const transactionEvent = await transactionEventPromise;
const spans = transactionEvent.spans || [];
const spans = await spansPromise;

expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.mysql',
description: 'SELECT 1 + 1 AS solution',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'mysql',
'db.query.text': 'SELECT 1 + 1 AS solution',
'db.user': 'root',
'db.connection_string': expect.any(String),
'server.address': expect.any(String),
'server.port': 3306,
for (const queryText of ['SELECT 1 + 1 AS solution', 'SELECT NOW()']) {
expect(spans).toContainEqual(
expect.objectContaining({
name: 'SELECT',
status: 'ok',
attributes: expect.objectContaining({
'sentry.op': { value: 'db', type: 'string' },
'sentry.origin': { value: 'auto.db.mysql', type: 'string' },
'db.system.name': { value: 'mysql', type: 'string' },
'db.query.text': { value: queryText, type: 'string' },
'db.user': { value: 'root', type: 'string' },
'db.connection_string': { value: expect.any(String), type: 'string' },
'server.address': { value: expect.any(String), type: 'string' },
'server.port': { value: 3306, type: 'integer' },
}),
}),
}),
);
expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.mysql',
description: 'SELECT NOW()',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'mysql',
'db.query.text': 'SELECT NOW()',
'db.user': 'root',
'db.connection_string': expect.any(String),
'server.address': expect.any(String),
'server.port': 3306,
}),
}),
);
);
}
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,6 @@ import { HydratedRouter } from 'react-router/dom';
const tracing = Sentry.reactRouterTracingIntegration({ useInstrumentationAPI: true });

Sentry.init({
traceLifecycle: 'static',
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: 'https://username@domain/123',
tunnel: `http://localhost:3031/`, // proxy server
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
import * as Sentry from '@sentry/react-router';

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://username@domain/123',
environment: 'qa', // dynamic sampling bias to keep transactions
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

test.describe('server - instrumentation API error capture', () => {
Expand All@@ -8,15 +8,15 @@ test.describe('server - instrumentation API error capture', () => {
return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-loader' && span.is_segment;
});

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

// Verify the error was captured with correct mechanism and transaction name
expect(error).toMatchObject({
Expand All@@ -36,58 +36,51 @@ test.describe('server - instrumentation API error capture', () => {
});

// Verify the transaction was also created with correct attributes
expect(transaction).toMatchObject({
transaction: 'GET /performance/error-loader',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('GET /performance/error-loader');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});

test('should include loader span in transaction even when loader throws', async ({ page }) => {
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
});
test('should include loader span in the segment even when loader throws', async ({ page }) => {
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/error-loader' && span.is_segment),
);

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const transaction = await txPromise;
const spans = await spansPromise;

// Find the loader span
const loaderSpan = transaction?.spans?.find(span => span.data?.['code.function.name'] === 'loader');
const loaderSpan = spans.find(span => span.attributes['code.function.name']?.value === 'loader');

expect(loaderSpan).toMatchObject({
data: {
'sentry.origin': 'auto.function.react_router.instrumentation_api',
'sentry.op': 'function',
'code.function.name': 'loader',
},
op: 'function',
expect(loaderSpan).toBeDefined();
expect(getSpanOp(loaderSpan!)).toBe('function');
expect(loaderSpan!.attributes).toMatchObject({
'sentry.origin': { value: 'auto.function.react_router.instrumentation_api', type: 'string' },
'sentry.op': { value: 'function', type: 'string' },
'code.function.name': { value: 'loader', type: 'string' },
});
});

test('error and transaction should share the same trace', async ({ page }) => {
test('error and segment span should share the same trace', async ({ page }) => {
const errorPromise = waitForError(APP_NAME, async errorEvent => {
return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-loader' && span.is_segment;
});

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

// Error and transaction should have the same trace_id
expect(error.contexts?.trace?.trace_id).toBe(transaction.contexts?.trace?.trace_id);
// Error and segment span should have the same trace_id
expect(error.contexts?.trace?.trace_id).toBe(span.trace_id);
});

// Skipped in dev: the action error is sometimes captured via the client instrumentation path
Expand All@@ -101,14 +94,14 @@ test.describe('server - instrumentation API error capture', () => {
return errorEvent.exception?.values?.[0]?.value === 'Action error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'POST /performance/error-action';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'POST /performance/error-action' && span.is_segment;
});

await page.goto(`/performance/error-action`);
await page.getByRole('button', { name: 'Trigger Error' }).click();

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

expect(error).toMatchObject({
exception: {
Expand All@@ -126,31 +119,25 @@ test.describe('server - instrumentation API error capture', () => {
transaction: 'POST /performance/error-action',
});

expect(transaction).toMatchObject({
transaction: 'POST /performance/error-action',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('POST /performance/error-action');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});

test('should capture middleware errors with instrumentation API mechanism', async ({ page }) => {
const errorPromise = waitForError(APP_NAME, async errorEvent => {
return errorEvent.exception?.values?.[0]?.value === 'Middleware error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-middleware';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-middleware' && span.is_segment;
});

await page.goto(`/performance/error-middleware`).catch(() => {
// Expected to fail due to middleware error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

expect(error).toMatchObject({
exception: {
Expand All@@ -168,14 +155,8 @@ test.describe('server - instrumentation API error capture', () => {
transaction: 'GET /performance/error-middleware',
});

expect(transaction).toMatchObject({
transaction: 'GET /performance/error-middleware',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('GET /performance/error-middleware');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});
});
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,94 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

// Same spans in both runs, from two injectors: the build-time transform in the server bundle, and
// the runtime hook in `react-router dev`, where the drivers stay on Node's own loader.
test.describe('server - orchestrion db instrumentation', () => {
test('instruments ioredis automatically via orchestrion', async ({ page }) => {
const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /performance/db-ioredis'
);
});
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/db-ioredis' && span.is_segment),
);

await page.goto('/performance/db-ioredis');

const transactionEvent = await transactionEventPromise;
const spans = transactionEvent.spans || [];
const spans = await spansPromise;
const segmentSpan = spans.find(span => span.name === 'GET /performance/db-ioredis' && span.is_segment)!;

// The server transaction must come from the native instrumentation API (not the legacy handler),
// The server segment must come from the native instrumentation API (not the legacy handler),
// proving the orchestrion-injected db spans share context with the React Router server span.
expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.react_router.instrumentation_api');
expect(getSpanOp(segmentSpan)).toBe('http.server');
expect(segmentSpan.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');

expect(spans).toContainEqual(
const childSpans = spans.filter(span => !span.is_segment);

expect(childSpans).toContainEqual(
expect.objectContaining({
op: 'db.query',
origin: 'auto.db.redis',
description: 'set test-key [1 other arguments]',
name: 'set test-key [1 other arguments]',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'redis',
'db.operation.name': 'set',
'db.query.text': 'set test-key [1 other arguments]',
attributes: expect.objectContaining({
'sentry.op': { value: 'db.query', type: 'string' },
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
'db.system.name': { value: 'redis', type: 'string' },
'db.operation.name': { value: 'set', type: 'string' },
'db.query.text': { value: 'set test-key [1 other arguments]', type: 'string' },
}),
}),
);
expect(spans).toContainEqual(
expect(childSpans).toContainEqual(
expect.objectContaining({
op: 'db.query',
origin: 'auto.db.redis',
description: 'get test-key',
name: 'get test-key',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'redis',
'db.operation.name': 'get',
'db.query.text': 'get test-key',
attributes: expect.objectContaining({
'sentry.op': { value: 'db.query', type: 'string' },
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
'db.system.name': { value: 'redis', type: 'string' },
'db.operation.name': { value: 'get', type: 'string' },
'db.query.text': { value: 'get test-key', type: 'string' },
}),
}),
);

// Each command maps to exactly one span (no offline-queue duplicate).
const setSpans = spans.filter(span => span.description === 'set test-key [1 other arguments]');
const setSpans = spans.filter(span => span.name === 'set test-key [1 other arguments]');
expect(setSpans).toHaveLength(1);

// Every db span nests under the native instrumentation-API http.server transaction.
const rootSpanId = transactionEvent.contexts?.trace?.span_id;
const spanIds = new Set([rootSpanId, ...spans.map(span => span.span_id)]);
const dbSpans = spans.filter(span => span.origin === 'auto.db.redis');
// Every db span nests under the native instrumentation-API http.server segment.
const spanIds = new Set(spans.filter(span => span.trace_id === segmentSpan.trace_id).map(span => span.span_id));
const dbSpans = spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.db.redis');
expect(dbSpans.every(span => typeof span.parent_span_id === 'string' && spanIds.has(span.parent_span_id))).toBe(
true,
);
});

// Under span streaming the mysql span name is the query summary, so both queries below are named
// `SELECT`. `db.query.text` is what tells them apart.
test('instruments mysql automatically via orchestrion', async ({ page }) => {
const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /performance/db-mysql'
);
});
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/db-mysql' && span.is_segment),
);

await page.goto('/performance/db-mysql');

const transactionEvent = await transactionEventPromise;
const spans = transactionEvent.spans || [];
const spans = await spansPromise;

expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.mysql',
description: 'SELECT 1 + 1 AS solution',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'mysql',
'db.query.text': 'SELECT 1 + 1 AS solution',
'db.user': 'root',
'db.connection_string': expect.any(String),
'server.address': expect.any(String),
'server.port': 3306,
for (const queryText of ['SELECT 1 + 1 AS solution', 'SELECT NOW()']) {
expect(spans).toContainEqual(
expect.objectContaining({
name: 'SELECT',
status: 'ok',
attributes: expect.objectContaining({
'sentry.op': { value: 'db', type: 'string' },
'sentry.origin': { value: 'auto.db.mysql', type: 'string' },
'db.system.name': { value: 'mysql', type: 'string' },
'db.query.text': { value: queryText, type: 'string' },
'db.user': { value: 'root', type: 'string' },
'db.connection_string': { value: expect.any(String), type: 'string' },
'server.address': { value: expect.any(String), type: 'string' },
'server.port': { value: 3306, type: 'integer' },
}),
}),
}),
);
expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.mysql',
description: 'SELECT NOW()',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'mysql',
'db.query.text': 'SELECT NOW()',
'db.user': 'root',
'db.connection_string': expect.any(String),
'server.address': expect.any(String),
'server.port': 3306,
}),
}),
);
);
}
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,6 @@ import { HydratedRouter } from 'react-router/dom';
const tracing = Sentry.reactRouterTracingIntegration({ useInstrumentationAPI: true });

Sentry.init({
traceLifecycle: 'static',
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: 'https://username@domain/123',
tunnel: `http://localhost:3031/`, // proxy server
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
import * as Sentry from '@sentry/react-router';

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://username@domain/123',
environment: 'qa', // dynamic sampling bias to keep transactions
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

test.describe('server - instrumentation API error capture', () => {
Expand All@@ -8,15 +8,15 @@ test.describe('server - instrumentation API error capture', () => {
return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-loader' && span.is_segment;
});

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

// Verify the error was captured with correct mechanism and transaction name
expect(error).toMatchObject({
Expand All@@ -36,58 +36,51 @@ test.describe('server - instrumentation API error capture', () => {
});

// Verify the transaction was also created with correct attributes
expect(transaction).toMatchObject({
transaction: 'GET /performance/error-loader',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('GET /performance/error-loader');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});

test('should include loader span in transaction even when loader throws', async ({ page }) => {
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
});
test('should include loader span in the segment even when loader throws', async ({ page }) => {
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/error-loader' && span.is_segment),
);

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const transaction = await txPromise;
const spans = await spansPromise;

// Find the loader span
const loaderSpan = transaction?.spans?.find(span => span.data?.['code.function.name'] === 'loader');
const loaderSpan = spans.find(span => span.attributes['code.function.name']?.value === 'loader');

expect(loaderSpan).toMatchObject({
data: {
'sentry.origin': 'auto.function.react_router.instrumentation_api',
'sentry.op': 'function',
'code.function.name': 'loader',
},
op: 'function',
expect(loaderSpan).toBeDefined();
expect(getSpanOp(loaderSpan!)).toBe('function');
expect(loaderSpan!.attributes).toMatchObject({
'sentry.origin': { value: 'auto.function.react_router.instrumentation_api', type: 'string' },
'sentry.op': { value: 'function', type: 'string' },
'code.function.name': { value: 'loader', type: 'string' },
});
});

test('error and transaction should share the same trace', async ({ page }) => {
test('error and segment span should share the same trace', async ({ page }) => {
const errorPromise = waitForError(APP_NAME, async errorEvent => {
return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-loader' && span.is_segment;
});

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

// Error and transaction should have the same trace_id
expect(error.contexts?.trace?.trace_id).toBe(transaction.contexts?.trace?.trace_id);
// Error and segment span should have the same trace_id
expect(error.contexts?.trace?.trace_id).toBe(span.trace_id);
});

// Skipped in dev: the action error is sometimes captured via the client instrumentation path
Expand All@@ -101,14 +94,14 @@ test.describe('server - instrumentation API error capture', () => {
return errorEvent.exception?.values?.[0]?.value === 'Action error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'POST /performance/error-action';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'POST /performance/error-action' && span.is_segment;
});

await page.goto(`/performance/error-action`);
await page.getByRole('button', { name: 'Trigger Error' }).click();

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

expect(error).toMatchObject({
exception: {
Expand All@@ -126,31 +119,25 @@ test.describe('server - instrumentation API error capture', () => {
transaction: 'POST /performance/error-action',
});

expect(transaction).toMatchObject({
transaction: 'POST /performance/error-action',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('POST /performance/error-action');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});

test('should capture middleware errors with instrumentation API mechanism', async ({ page }) => {
const errorPromise = waitForError(APP_NAME, async errorEvent => {
return errorEvent.exception?.values?.[0]?.value === 'Middleware error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-middleware';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-middleware' && span.is_segment;
});

await page.goto(`/performance/error-middleware`).catch(() => {
// Expected to fail due to middleware error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

expect(error).toMatchObject({
exception: {
Expand All@@ -168,14 +155,8 @@ test.describe('server - instrumentation API error capture', () => {
transaction: 'GET /performance/error-middleware',
});

expect(transaction).toMatchObject({
transaction: 'GET /performance/error-middleware',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('GET /performance/error-middleware');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});
});
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,94 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

// Same spans in both runs, from two injectors: the build-time transform in the server bundle, and
// the runtime hook in `react-router dev`, where the drivers stay on Node's own loader.
test.describe('server - orchestrion db instrumentation', () => {
test('instruments ioredis automatically via orchestrion', async ({ page }) => {
const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /performance/db-ioredis'
);
});
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/db-ioredis' && span.is_segment),
);

await page.goto('/performance/db-ioredis');

const transactionEvent = await transactionEventPromise;
const spans = transactionEvent.spans || [];
const spans = await spansPromise;
const segmentSpan = spans.find(span => span.name === 'GET /performance/db-ioredis' && span.is_segment)!;

// The server transaction must come from the native instrumentation API (not the legacy handler),
// The server segment must come from the native instrumentation API (not the legacy handler),
// proving the orchestrion-injected db spans share context with the React Router server span.
expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.react_router.instrumentation_api');
expect(getSpanOp(segmentSpan)).toBe('http.server');
expect(segmentSpan.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');

expect(spans).toContainEqual(
const childSpans = spans.filter(span => !span.is_segment);

expect(childSpans).toContainEqual(
expect.objectContaining({
op: 'db.query',
origin: 'auto.db.redis',
description: 'set test-key [1 other arguments]',
name: 'set test-key [1 other arguments]',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'redis',
'db.operation.name': 'set',
'db.query.text': 'set test-key [1 other arguments]',
attributes: expect.objectContaining({
'sentry.op': { value: 'db.query', type: 'string' },
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
'db.system.name': { value: 'redis', type: 'string' },
'db.operation.name': { value: 'set', type: 'string' },
'db.query.text': { value: 'set test-key [1 other arguments]', type: 'string' },
}),
}),
);
expect(spans).toContainEqual(
expect(childSpans).toContainEqual(
expect.objectContaining({
op: 'db.query',
origin: 'auto.db.redis',
description: 'get test-key',
name: 'get test-key',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'redis',
'db.operation.name': 'get',
'db.query.text': 'get test-key',
attributes: expect.objectContaining({
'sentry.op': { value: 'db.query', type: 'string' },
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
'db.system.name': { value: 'redis', type: 'string' },
'db.operation.name': { value: 'get', type: 'string' },
'db.query.text': { value: 'get test-key', type: 'string' },
}),
}),
);

// Each command maps to exactly one span (no offline-queue duplicate).
const setSpans = spans.filter(span => span.description === 'set test-key [1 other arguments]');
const setSpans = spans.filter(span => span.name === 'set test-key [1 other arguments]');
expect(setSpans).toHaveLength(1);

// Every db span nests under the native instrumentation-API http.server transaction.
const rootSpanId = transactionEvent.contexts?.trace?.span_id;
const spanIds = new Set([rootSpanId, ...spans.map(span => span.span_id)]);
const dbSpans = spans.filter(span => span.origin === 'auto.db.redis');
// Every db span nests under the native instrumentation-API http.server segment.
const spanIds = new Set(spans.filter(span => span.trace_id === segmentSpan.trace_id).map(span => span.span_id));
const dbSpans = spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.db.redis');
expect(dbSpans.every(span => typeof span.parent_span_id === 'string' && spanIds.has(span.parent_span_id))).toBe(
true,
);
});

// Under span streaming the mysql span name is the query summary, so both queries below are named
// `SELECT`. `db.query.text` is what tells them apart.
test('instruments mysql automatically via orchestrion', async ({ page }) => {
const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /performance/db-mysql'
);
});
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/db-mysql' && span.is_segment),
);

await page.goto('/performance/db-mysql');

const transactionEvent = await transactionEventPromise;
const spans = transactionEvent.spans || [];
const spans = await spansPromise;

expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.mysql',
description: 'SELECT 1 + 1 AS solution',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'mysql',
'db.query.text': 'SELECT 1 + 1 AS solution',
'db.user': 'root',
'db.connection_string': expect.any(String),
'server.address': expect.any(String),
'server.port': 3306,
for (const queryText of ['SELECT 1 + 1 AS solution', 'SELECT NOW()']) {
expect(spans).toContainEqual(
expect.objectContaining({
name: 'SELECT',
status: 'ok',
attributes: expect.objectContaining({
'sentry.op': { value: 'db', type: 'string' },
'sentry.origin': { value: 'auto.db.mysql', type: 'string' },
'db.system.name': { value: 'mysql', type: 'string' },
'db.query.text': { value: queryText, type: 'string' },
'db.user': { value: 'root', type: 'string' },
'db.connection_string': { value: expect.any(String), type: 'string' },
'server.address': { value: expect.any(String), type: 'string' },
'server.port': { value: 3306, type: 'integer' },
}),
}),
}),
);
expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.mysql',
description: 'SELECT NOW()',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'mysql',
'db.query.text': 'SELECT NOW()',
'db.user': 'root',
'db.connection_string': expect.any(String),
'server.address': expect.any(String),
'server.port': 3306,
}),
}),
);
);
}
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,6 @@ import { HydratedRouter } from 'react-router/dom';
const tracing = Sentry.reactRouterTracingIntegration({ useInstrumentationAPI: true });

Sentry.init({
traceLifecycle: 'static',
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: 'https://username@domain/123',
tunnel: `http://localhost:3031/`, // proxy server
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
import * as Sentry from '@sentry/react-router';

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://username@domain/123',
environment: 'qa', // dynamic sampling bias to keep transactions
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

test.describe('server - instrumentation API error capture', () => {
Expand All@@ -8,15 +8,15 @@ test.describe('server - instrumentation API error capture', () => {
return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-loader' && span.is_segment;
});

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

// Verify the error was captured with correct mechanism and transaction name
expect(error).toMatchObject({
Expand All@@ -36,58 +36,51 @@ test.describe('server - instrumentation API error capture', () => {
});

// Verify the transaction was also created with correct attributes
expect(transaction).toMatchObject({
transaction: 'GET /performance/error-loader',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('GET /performance/error-loader');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});

test('should include loader span in transaction even when loader throws', async ({ page }) => {
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
});
test('should include loader span in the segment even when loader throws', async ({ page }) => {
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/error-loader' && span.is_segment),
);

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const transaction = await txPromise;
const spans = await spansPromise;

// Find the loader span
const loaderSpan = transaction?.spans?.find(span => span.data?.['code.function.name'] === 'loader');
const loaderSpan = spans.find(span => span.attributes['code.function.name']?.value === 'loader');

expect(loaderSpan).toMatchObject({
data: {
'sentry.origin': 'auto.function.react_router.instrumentation_api',
'sentry.op': 'function',
'code.function.name': 'loader',
},
op: 'function',
expect(loaderSpan).toBeDefined();
expect(getSpanOp(loaderSpan!)).toBe('function');
expect(loaderSpan!.attributes).toMatchObject({
'sentry.origin': { value: 'auto.function.react_router.instrumentation_api', type: 'string' },
'sentry.op': { value: 'function', type: 'string' },
'code.function.name': { value: 'loader', type: 'string' },
});
});

test('error and transaction should share the same trace', async ({ page }) => {
test('error and segment span should share the same trace', async ({ page }) => {
const errorPromise = waitForError(APP_NAME, async errorEvent => {
return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-loader' && span.is_segment;
});

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

// Error and transaction should have the same trace_id
expect(error.contexts?.trace?.trace_id).toBe(transaction.contexts?.trace?.trace_id);
// Error and segment span should have the same trace_id
expect(error.contexts?.trace?.trace_id).toBe(span.trace_id);
});

// Skipped in dev: the action error is sometimes captured via the client instrumentation path
Expand All@@ -101,14 +94,14 @@ test.describe('server - instrumentation API error capture', () => {
return errorEvent.exception?.values?.[0]?.value === 'Action error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'POST /performance/error-action';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'POST /performance/error-action' && span.is_segment;
});

await page.goto(`/performance/error-action`);
await page.getByRole('button', { name: 'Trigger Error' }).click();

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

expect(error).toMatchObject({
exception: {
Expand All@@ -126,31 +119,25 @@ test.describe('server - instrumentation API error capture', () => {
transaction: 'POST /performance/error-action',
});

expect(transaction).toMatchObject({
transaction: 'POST /performance/error-action',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('POST /performance/error-action');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});

test('should capture middleware errors with instrumentation API mechanism', async ({ page }) => {
const errorPromise = waitForError(APP_NAME, async errorEvent => {
return errorEvent.exception?.values?.[0]?.value === 'Middleware error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-middleware';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-middleware' && span.is_segment;
});

await page.goto(`/performance/error-middleware`).catch(() => {
// Expected to fail due to middleware error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

expect(error).toMatchObject({
exception: {
Expand All@@ -168,14 +155,8 @@ test.describe('server - instrumentation API error capture', () => {
transaction: 'GET /performance/error-middleware',
});

expect(transaction).toMatchObject({
transaction: 'GET /performance/error-middleware',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('GET /performance/error-middleware');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});
});
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,94 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

// Same spans in both runs, from two injectors: the build-time transform in the server bundle, and
// the runtime hook in `react-router dev`, where the drivers stay on Node's own loader.
test.describe('server - orchestrion db instrumentation', () => {
test('instruments ioredis automatically via orchestrion', async ({ page }) => {
const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /performance/db-ioredis'
);
});
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/db-ioredis' && span.is_segment),
);

await page.goto('/performance/db-ioredis');

const transactionEvent = await transactionEventPromise;
const spans = transactionEvent.spans || [];
const spans = await spansPromise;
const segmentSpan = spans.find(span => span.name === 'GET /performance/db-ioredis' && span.is_segment)!;

// The server transaction must come from the native instrumentation API (not the legacy handler),
// The server segment must come from the native instrumentation API (not the legacy handler),
// proving the orchestrion-injected db spans share context with the React Router server span.
expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.react_router.instrumentation_api');
expect(getSpanOp(segmentSpan)).toBe('http.server');
expect(segmentSpan.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');

expect(spans).toContainEqual(
const childSpans = spans.filter(span => !span.is_segment);

expect(childSpans).toContainEqual(
expect.objectContaining({
op: 'db.query',
origin: 'auto.db.redis',
description: 'set test-key [1 other arguments]',
name: 'set test-key [1 other arguments]',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'redis',
'db.operation.name': 'set',
'db.query.text': 'set test-key [1 other arguments]',
attributes: expect.objectContaining({
'sentry.op': { value: 'db.query', type: 'string' },
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
'db.system.name': { value: 'redis', type: 'string' },
'db.operation.name': { value: 'set', type: 'string' },
'db.query.text': { value: 'set test-key [1 other arguments]', type: 'string' },
}),
}),
);
expect(spans).toContainEqual(
expect(childSpans).toContainEqual(
expect.objectContaining({
op: 'db.query',
origin: 'auto.db.redis',
description: 'get test-key',
name: 'get test-key',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'redis',
'db.operation.name': 'get',
'db.query.text': 'get test-key',
attributes: expect.objectContaining({
'sentry.op': { value: 'db.query', type: 'string' },
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
'db.system.name': { value: 'redis', type: 'string' },
'db.operation.name': { value: 'get', type: 'string' },
'db.query.text': { value: 'get test-key', type: 'string' },
}),
}),
);

// Each command maps to exactly one span (no offline-queue duplicate).
const setSpans = spans.filter(span => span.description === 'set test-key [1 other arguments]');
const setSpans = spans.filter(span => span.name === 'set test-key [1 other arguments]');
expect(setSpans).toHaveLength(1);

// Every db span nests under the native instrumentation-API http.server transaction.
const rootSpanId = transactionEvent.contexts?.trace?.span_id;
const spanIds = new Set([rootSpanId, ...spans.map(span => span.span_id)]);
const dbSpans = spans.filter(span => span.origin === 'auto.db.redis');
// Every db span nests under the native instrumentation-API http.server segment.
const spanIds = new Set(spans.filter(span => span.trace_id === segmentSpan.trace_id).map(span => span.span_id));
const dbSpans = spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.db.redis');
expect(dbSpans.every(span => typeof span.parent_span_id === 'string' && spanIds.has(span.parent_span_id))).toBe(
true,
);
});

// Under span streaming the mysql span name is the query summary, so both queries below are named
// `SELECT`. `db.query.text` is what tells them apart.
test('instruments mysql automatically via orchestrion', async ({ page }) => {
const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /performance/db-mysql'
);
});
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/db-mysql' && span.is_segment),
);

await page.goto('/performance/db-mysql');

const transactionEvent = await transactionEventPromise;
const spans = transactionEvent.spans || [];
const spans = await spansPromise;

expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.mysql',
description: 'SELECT 1 + 1 AS solution',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'mysql',
'db.query.text': 'SELECT 1 + 1 AS solution',
'db.user': 'root',
'db.connection_string': expect.any(String),
'server.address': expect.any(String),
'server.port': 3306,
for (const queryText of ['SELECT 1 + 1 AS solution', 'SELECT NOW()']) {
expect(spans).toContainEqual(
expect.objectContaining({
name: 'SELECT',
status: 'ok',
attributes: expect.objectContaining({
'sentry.op': { value: 'db', type: 'string' },
'sentry.origin': { value: 'auto.db.mysql', type: 'string' },
'db.system.name': { value: 'mysql', type: 'string' },
'db.query.text': { value: queryText, type: 'string' },
'db.user': { value: 'root', type: 'string' },
'db.connection_string': { value: expect.any(String), type: 'string' },
'server.address': { value: expect.any(String), type: 'string' },
'server.port': { value: 3306, type: 'integer' },
}),
}),
}),
);
expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.mysql',
description: 'SELECT NOW()',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'mysql',
'db.query.text': 'SELECT NOW()',
'db.user': 'root',
'db.connection_string': expect.any(String),
'server.address': expect.any(String),
'server.port': 3306,
}),
}),
);
);
}
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,6 @@ import { HydratedRouter } from 'react-router/dom';
const tracing = Sentry.reactRouterTracingIntegration({ useInstrumentationAPI: true });

Sentry.init({
traceLifecycle: 'static',
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: 'https://username@domain/123',
tunnel: `http://localhost:3031/`, // proxy server
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
import * as Sentry from '@sentry/react-router';

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://username@domain/123',
environment: 'qa', // dynamic sampling bias to keep transactions
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

test.describe('server - instrumentation API error capture', () => {
Expand All@@ -8,15 +8,15 @@ test.describe('server - instrumentation API error capture', () => {
return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-loader' && span.is_segment;
});

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

// Verify the error was captured with correct mechanism and transaction name
expect(error).toMatchObject({
Expand All@@ -36,58 +36,51 @@ test.describe('server - instrumentation API error capture', () => {
});

// Verify the transaction was also created with correct attributes
expect(transaction).toMatchObject({
transaction: 'GET /performance/error-loader',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('GET /performance/error-loader');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});

test('should include loader span in transaction even when loader throws', async ({ page }) => {
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
});
test('should include loader span in the segment even when loader throws', async ({ page }) => {
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/error-loader' && span.is_segment),
);

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const transaction = await txPromise;
const spans = await spansPromise;

// Find the loader span
const loaderSpan = transaction?.spans?.find(span => span.data?.['code.function.name'] === 'loader');
const loaderSpan = spans.find(span => span.attributes['code.function.name']?.value === 'loader');

expect(loaderSpan).toMatchObject({
data: {
'sentry.origin': 'auto.function.react_router.instrumentation_api',
'sentry.op': 'function',
'code.function.name': 'loader',
},
op: 'function',
expect(loaderSpan).toBeDefined();
expect(getSpanOp(loaderSpan!)).toBe('function');
expect(loaderSpan!.attributes).toMatchObject({
'sentry.origin': { value: 'auto.function.react_router.instrumentation_api', type: 'string' },
'sentry.op': { value: 'function', type: 'string' },
'code.function.name': { value: 'loader', type: 'string' },
});
});

test('error and transaction should share the same trace', async ({ page }) => {
test('error and segment span should share the same trace', async ({ page }) => {
const errorPromise = waitForError(APP_NAME, async errorEvent => {
return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-loader' && span.is_segment;
});

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

// Error and transaction should have the same trace_id
expect(error.contexts?.trace?.trace_id).toBe(transaction.contexts?.trace?.trace_id);
// Error and segment span should have the same trace_id
expect(error.contexts?.trace?.trace_id).toBe(span.trace_id);
});

// Skipped in dev: the action error is sometimes captured via the client instrumentation path
Expand All@@ -101,14 +94,14 @@ test.describe('server - instrumentation API error capture', () => {
return errorEvent.exception?.values?.[0]?.value === 'Action error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'POST /performance/error-action';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'POST /performance/error-action' && span.is_segment;
});

await page.goto(`/performance/error-action`);
await page.getByRole('button', { name: 'Trigger Error' }).click();

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

expect(error).toMatchObject({
exception: {
Expand All@@ -126,31 +119,25 @@ test.describe('server - instrumentation API error capture', () => {
transaction: 'POST /performance/error-action',
});

expect(transaction).toMatchObject({
transaction: 'POST /performance/error-action',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('POST /performance/error-action');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});

test('should capture middleware errors with instrumentation API mechanism', async ({ page }) => {
const errorPromise = waitForError(APP_NAME, async errorEvent => {
return errorEvent.exception?.values?.[0]?.value === 'Middleware error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-middleware';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-middleware' && span.is_segment;
});

await page.goto(`/performance/error-middleware`).catch(() => {
// Expected to fail due to middleware error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

expect(error).toMatchObject({
exception: {
Expand All@@ -168,14 +155,8 @@ test.describe('server - instrumentation API error capture', () => {
transaction: 'GET /performance/error-middleware',
});

expect(transaction).toMatchObject({
transaction: 'GET /performance/error-middleware',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('GET /performance/error-middleware');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});
});
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,94 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

// Same spans in both runs, from two injectors: the build-time transform in the server bundle, and
// the runtime hook in `react-router dev`, where the drivers stay on Node's own loader.
test.describe('server - orchestrion db instrumentation', () => {
test('instruments ioredis automatically via orchestrion', async ({ page }) => {
const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /performance/db-ioredis'
);
});
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/db-ioredis' && span.is_segment),
);

await page.goto('/performance/db-ioredis');

const transactionEvent = await transactionEventPromise;
const spans = transactionEvent.spans || [];
const spans = await spansPromise;
const segmentSpan = spans.find(span => span.name === 'GET /performance/db-ioredis' && span.is_segment)!;

// The server transaction must come from the native instrumentation API (not the legacy handler),
// The server segment must come from the native instrumentation API (not the legacy handler),
// proving the orchestrion-injected db spans share context with the React Router server span.
expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.react_router.instrumentation_api');
expect(getSpanOp(segmentSpan)).toBe('http.server');
expect(segmentSpan.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');

expect(spans).toContainEqual(
const childSpans = spans.filter(span => !span.is_segment);

expect(childSpans).toContainEqual(
expect.objectContaining({
op: 'db.query',
origin: 'auto.db.redis',
description: 'set test-key [1 other arguments]',
name: 'set test-key [1 other arguments]',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'redis',
'db.operation.name': 'set',
'db.query.text': 'set test-key [1 other arguments]',
attributes: expect.objectContaining({
'sentry.op': { value: 'db.query', type: 'string' },
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
'db.system.name': { value: 'redis', type: 'string' },
'db.operation.name': { value: 'set', type: 'string' },
'db.query.text': { value: 'set test-key [1 other arguments]', type: 'string' },
}),
}),
);
expect(spans).toContainEqual(
expect(childSpans).toContainEqual(
expect.objectContaining({
op: 'db.query',
origin: 'auto.db.redis',
description: 'get test-key',
name: 'get test-key',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'redis',
'db.operation.name': 'get',
'db.query.text': 'get test-key',
attributes: expect.objectContaining({
'sentry.op': { value: 'db.query', type: 'string' },
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
'db.system.name': { value: 'redis', type: 'string' },
'db.operation.name': { value: 'get', type: 'string' },
'db.query.text': { value: 'get test-key', type: 'string' },
}),
}),
);

// Each command maps to exactly one span (no offline-queue duplicate).
const setSpans = spans.filter(span => span.description === 'set test-key [1 other arguments]');
const setSpans = spans.filter(span => span.name === 'set test-key [1 other arguments]');
expect(setSpans).toHaveLength(1);

// Every db span nests under the native instrumentation-API http.server transaction.
const rootSpanId = transactionEvent.contexts?.trace?.span_id;
const spanIds = new Set([rootSpanId, ...spans.map(span => span.span_id)]);
const dbSpans = spans.filter(span => span.origin === 'auto.db.redis');
// Every db span nests under the native instrumentation-API http.server segment.
const spanIds = new Set(spans.filter(span => span.trace_id === segmentSpan.trace_id).map(span => span.span_id));
const dbSpans = spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.db.redis');
expect(dbSpans.every(span => typeof span.parent_span_id === 'string' && spanIds.has(span.parent_span_id))).toBe(
true,
);
});

// Under span streaming the mysql span name is the query summary, so both queries below are named
// `SELECT`. `db.query.text` is what tells them apart.
test('instruments mysql automatically via orchestrion', async ({ page }) => {
const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /performance/db-mysql'
);
});
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/db-mysql' && span.is_segment),
);

await page.goto('/performance/db-mysql');

const transactionEvent = await transactionEventPromise;
const spans = transactionEvent.spans || [];
const spans = await spansPromise;

expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.mysql',
description: 'SELECT 1 + 1 AS solution',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'mysql',
'db.query.text': 'SELECT 1 + 1 AS solution',
'db.user': 'root',
'db.connection_string': expect.any(String),
'server.address': expect.any(String),
'server.port': 3306,
for (const queryText of ['SELECT 1 + 1 AS solution', 'SELECT NOW()']) {
expect(spans).toContainEqual(
expect.objectContaining({
name: 'SELECT',
status: 'ok',
attributes: expect.objectContaining({
'sentry.op': { value: 'db', type: 'string' },
'sentry.origin': { value: 'auto.db.mysql', type: 'string' },
'db.system.name': { value: 'mysql', type: 'string' },
'db.query.text': { value: queryText, type: 'string' },
'db.user': { value: 'root', type: 'string' },
'db.connection_string': { value: expect.any(String), type: 'string' },
'server.address': { value: expect.any(String), type: 'string' },
'server.port': { value: 3306, type: 'integer' },
}),
}),
}),
);
expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.mysql',
description: 'SELECT NOW()',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'mysql',
'db.query.text': 'SELECT NOW()',
'db.user': 'root',
'db.connection_string': expect.any(String),
'server.address': expect.any(String),
'server.port': 3306,
}),
}),
);
);
}
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,6 @@ import { HydratedRouter } from 'react-router/dom';
const tracing = Sentry.reactRouterTracingIntegration({ useInstrumentationAPI: true });

Sentry.init({
traceLifecycle: 'static',
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: 'https://username@domain/123',
tunnel: `http://localhost:3031/`, // proxy server
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
import * as Sentry from '@sentry/react-router';

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://username@domain/123',
environment: 'qa', // dynamic sampling bias to keep transactions
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

test.describe('server - instrumentation API error capture', () => {
Expand All@@ -8,15 +8,15 @@ test.describe('server - instrumentation API error capture', () => {
return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-loader' && span.is_segment;
});

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

// Verify the error was captured with correct mechanism and transaction name
expect(error).toMatchObject({
Expand All@@ -36,58 +36,51 @@ test.describe('server - instrumentation API error capture', () => {
});

// Verify the transaction was also created with correct attributes
expect(transaction).toMatchObject({
transaction: 'GET /performance/error-loader',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('GET /performance/error-loader');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});

test('should include loader span in transaction even when loader throws', async ({ page }) => {
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
});
test('should include loader span in the segment even when loader throws', async ({ page }) => {
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/error-loader' && span.is_segment),
);

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const transaction = await txPromise;
const spans = await spansPromise;

// Find the loader span
const loaderSpan = transaction?.spans?.find(span => span.data?.['code.function.name'] === 'loader');
const loaderSpan = spans.find(span => span.attributes['code.function.name']?.value === 'loader');

expect(loaderSpan).toMatchObject({
data: {
'sentry.origin': 'auto.function.react_router.instrumentation_api',
'sentry.op': 'function',
'code.function.name': 'loader',
},
op: 'function',
expect(loaderSpan).toBeDefined();
expect(getSpanOp(loaderSpan!)).toBe('function');
expect(loaderSpan!.attributes).toMatchObject({
'sentry.origin': { value: 'auto.function.react_router.instrumentation_api', type: 'string' },
'sentry.op': { value: 'function', type: 'string' },
'code.function.name': { value: 'loader', type: 'string' },
});
});

test('error and transaction should share the same trace', async ({ page }) => {
test('error and segment span should share the same trace', async ({ page }) => {
const errorPromise = waitForError(APP_NAME, async errorEvent => {
return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-loader' && span.is_segment;
});

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

// Error and transaction should have the same trace_id
expect(error.contexts?.trace?.trace_id).toBe(transaction.contexts?.trace?.trace_id);
// Error and segment span should have the same trace_id
expect(error.contexts?.trace?.trace_id).toBe(span.trace_id);
});

// Skipped in dev: the action error is sometimes captured via the client instrumentation path
Expand All@@ -101,14 +94,14 @@ test.describe('server - instrumentation API error capture', () => {
return errorEvent.exception?.values?.[0]?.value === 'Action error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'POST /performance/error-action';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'POST /performance/error-action' && span.is_segment;
});

await page.goto(`/performance/error-action`);
await page.getByRole('button', { name: 'Trigger Error' }).click();

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

expect(error).toMatchObject({
exception: {
Expand All@@ -126,31 +119,25 @@ test.describe('server - instrumentation API error capture', () => {
transaction: 'POST /performance/error-action',
});

expect(transaction).toMatchObject({
transaction: 'POST /performance/error-action',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('POST /performance/error-action');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});

test('should capture middleware errors with instrumentation API mechanism', async ({ page }) => {
const errorPromise = waitForError(APP_NAME, async errorEvent => {
return errorEvent.exception?.values?.[0]?.value === 'Middleware error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-middleware';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-middleware' && span.is_segment;
});

await page.goto(`/performance/error-middleware`).catch(() => {
// Expected to fail due to middleware error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

expect(error).toMatchObject({
exception: {
Expand All@@ -168,14 +155,8 @@ test.describe('server - instrumentation API error capture', () => {
transaction: 'GET /performance/error-middleware',
});

expect(transaction).toMatchObject({
transaction: 'GET /performance/error-middleware',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('GET /performance/error-middleware');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});
});
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,94 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

// Same spans in both runs, from two injectors: the build-time transform in the server bundle, and
// the runtime hook in `react-router dev`, where the drivers stay on Node's own loader.
test.describe('server - orchestrion db instrumentation', () => {
test('instruments ioredis automatically via orchestrion', async ({ page }) => {
const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /performance/db-ioredis'
);
});
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/db-ioredis' && span.is_segment),
);

await page.goto('/performance/db-ioredis');

const transactionEvent = await transactionEventPromise;
const spans = transactionEvent.spans || [];
const spans = await spansPromise;
const segmentSpan = spans.find(span => span.name === 'GET /performance/db-ioredis' && span.is_segment)!;

// The server transaction must come from the native instrumentation API (not the legacy handler),
// The server segment must come from the native instrumentation API (not the legacy handler),
// proving the orchestrion-injected db spans share context with the React Router server span.
expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.react_router.instrumentation_api');
expect(getSpanOp(segmentSpan)).toBe('http.server');
expect(segmentSpan.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');

expect(spans).toContainEqual(
const childSpans = spans.filter(span => !span.is_segment);

expect(childSpans).toContainEqual(
expect.objectContaining({
op: 'db.query',
origin: 'auto.db.redis',
description: 'set test-key [1 other arguments]',
name: 'set test-key [1 other arguments]',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'redis',
'db.operation.name': 'set',
'db.query.text': 'set test-key [1 other arguments]',
attributes: expect.objectContaining({
'sentry.op': { value: 'db.query', type: 'string' },
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
'db.system.name': { value: 'redis', type: 'string' },
'db.operation.name': { value: 'set', type: 'string' },
'db.query.text': { value: 'set test-key [1 other arguments]', type: 'string' },
}),
}),
);
expect(spans).toContainEqual(
expect(childSpans).toContainEqual(
expect.objectContaining({
op: 'db.query',
origin: 'auto.db.redis',
description: 'get test-key',
name: 'get test-key',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'redis',
'db.operation.name': 'get',
'db.query.text': 'get test-key',
attributes: expect.objectContaining({
'sentry.op': { value: 'db.query', type: 'string' },
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
'db.system.name': { value: 'redis', type: 'string' },
'db.operation.name': { value: 'get', type: 'string' },
'db.query.text': { value: 'get test-key', type: 'string' },
}),
}),
);

// Each command maps to exactly one span (no offline-queue duplicate).
const setSpans = spans.filter(span => span.description === 'set test-key [1 other arguments]');
const setSpans = spans.filter(span => span.name === 'set test-key [1 other arguments]');
expect(setSpans).toHaveLength(1);

// Every db span nests under the native instrumentation-API http.server transaction.
const rootSpanId = transactionEvent.contexts?.trace?.span_id;
const spanIds = new Set([rootSpanId, ...spans.map(span => span.span_id)]);
const dbSpans = spans.filter(span => span.origin === 'auto.db.redis');
// Every db span nests under the native instrumentation-API http.server segment.
const spanIds = new Set(spans.filter(span => span.trace_id === segmentSpan.trace_id).map(span => span.span_id));
const dbSpans = spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.db.redis');
expect(dbSpans.every(span => typeof span.parent_span_id === 'string' && spanIds.has(span.parent_span_id))).toBe(
true,
);
});

// Under span streaming the mysql span name is the query summary, so both queries below are named
// `SELECT`. `db.query.text` is what tells them apart.
test('instruments mysql automatically via orchestrion', async ({ page }) => {
const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /performance/db-mysql'
);
});
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/db-mysql' && span.is_segment),
);

await page.goto('/performance/db-mysql');

const transactionEvent = await transactionEventPromise;
const spans = transactionEvent.spans || [];
const spans = await spansPromise;

expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.mysql',
description: 'SELECT 1 + 1 AS solution',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'mysql',
'db.query.text': 'SELECT 1 + 1 AS solution',
'db.user': 'root',
'db.connection_string': expect.any(String),
'server.address': expect.any(String),
'server.port': 3306,
for (const queryText of ['SELECT 1 + 1 AS solution', 'SELECT NOW()']) {
expect(spans).toContainEqual(
expect.objectContaining({
name: 'SELECT',
status: 'ok',
attributes: expect.objectContaining({
'sentry.op': { value: 'db', type: 'string' },
'sentry.origin': { value: 'auto.db.mysql', type: 'string' },
'db.system.name': { value: 'mysql', type: 'string' },
'db.query.text': { value: queryText, type: 'string' },
'db.user': { value: 'root', type: 'string' },
'db.connection_string': { value: expect.any(String), type: 'string' },
'server.address': { value: expect.any(String), type: 'string' },
'server.port': { value: 3306, type: 'integer' },
}),
}),
}),
);
expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.mysql',
description: 'SELECT NOW()',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'mysql',
'db.query.text': 'SELECT NOW()',
'db.user': 'root',
'db.connection_string': expect.any(String),
'server.address': expect.any(String),
'server.port': 3306,
}),
}),
);
);
}
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,6 @@ import { HydratedRouter } from 'react-router/dom';
const tracing = Sentry.reactRouterTracingIntegration({ useInstrumentationAPI: true });

Sentry.init({
traceLifecycle: 'static',
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: 'https://username@domain/123',
tunnel: `http://localhost:3031/`, // proxy server
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
import * as Sentry from '@sentry/react-router';

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://username@domain/123',
environment: 'qa', // dynamic sampling bias to keep transactions
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

test.describe('server - instrumentation API error capture', () => {
Expand All@@ -8,15 +8,15 @@ test.describe('server - instrumentation API error capture', () => {
return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-loader' && span.is_segment;
});

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

// Verify the error was captured with correct mechanism and transaction name
expect(error).toMatchObject({
Expand All@@ -36,58 +36,51 @@ test.describe('server - instrumentation API error capture', () => {
});

// Verify the transaction was also created with correct attributes
expect(transaction).toMatchObject({
transaction: 'GET /performance/error-loader',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('GET /performance/error-loader');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});

test('should include loader span in transaction even when loader throws', async ({ page }) => {
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
});
test('should include loader span in the segment even when loader throws', async ({ page }) => {
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/error-loader' && span.is_segment),
);

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const transaction = await txPromise;
const spans = await spansPromise;

// Find the loader span
const loaderSpan = transaction?.spans?.find(span => span.data?.['code.function.name'] === 'loader');
const loaderSpan = spans.find(span => span.attributes['code.function.name']?.value === 'loader');

expect(loaderSpan).toMatchObject({
data: {
'sentry.origin': 'auto.function.react_router.instrumentation_api',
'sentry.op': 'function',
'code.function.name': 'loader',
},
op: 'function',
expect(loaderSpan).toBeDefined();
expect(getSpanOp(loaderSpan!)).toBe('function');
expect(loaderSpan!.attributes).toMatchObject({
'sentry.origin': { value: 'auto.function.react_router.instrumentation_api', type: 'string' },
'sentry.op': { value: 'function', type: 'string' },
'code.function.name': { value: 'loader', type: 'string' },
});
});

test('error and transaction should share the same trace', async ({ page }) => {
test('error and segment span should share the same trace', async ({ page }) => {
const errorPromise = waitForError(APP_NAME, async errorEvent => {
return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-loader' && span.is_segment;
});

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

// Error and transaction should have the same trace_id
expect(error.contexts?.trace?.trace_id).toBe(transaction.contexts?.trace?.trace_id);
// Error and segment span should have the same trace_id
expect(error.contexts?.trace?.trace_id).toBe(span.trace_id);
});

// Skipped in dev: the action error is sometimes captured via the client instrumentation path
Expand All@@ -101,14 +94,14 @@ test.describe('server - instrumentation API error capture', () => {
return errorEvent.exception?.values?.[0]?.value === 'Action error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'POST /performance/error-action';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'POST /performance/error-action' && span.is_segment;
});

await page.goto(`/performance/error-action`);
await page.getByRole('button', { name: 'Trigger Error' }).click();

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

expect(error).toMatchObject({
exception: {
Expand All@@ -126,31 +119,25 @@ test.describe('server - instrumentation API error capture', () => {
transaction: 'POST /performance/error-action',
});

expect(transaction).toMatchObject({
transaction: 'POST /performance/error-action',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('POST /performance/error-action');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});

test('should capture middleware errors with instrumentation API mechanism', async ({ page }) => {
const errorPromise = waitForError(APP_NAME, async errorEvent => {
return errorEvent.exception?.values?.[0]?.value === 'Middleware error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-middleware';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-middleware' && span.is_segment;
});

await page.goto(`/performance/error-middleware`).catch(() => {
// Expected to fail due to middleware error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

expect(error).toMatchObject({
exception: {
Expand All@@ -168,14 +155,8 @@ test.describe('server - instrumentation API error capture', () => {
transaction: 'GET /performance/error-middleware',
});

expect(transaction).toMatchObject({
transaction: 'GET /performance/error-middleware',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('GET /performance/error-middleware');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});
});
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,94 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

// Same spans in both runs, from two injectors: the build-time transform in the server bundle, and
// the runtime hook in `react-router dev`, where the drivers stay on Node's own loader.
test.describe('server - orchestrion db instrumentation', () => {
test('instruments ioredis automatically via orchestrion', async ({ page }) => {
const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /performance/db-ioredis'
);
});
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/db-ioredis' && span.is_segment),
);

await page.goto('/performance/db-ioredis');

const transactionEvent = await transactionEventPromise;
const spans = transactionEvent.spans || [];
const spans = await spansPromise;
const segmentSpan = spans.find(span => span.name === 'GET /performance/db-ioredis' && span.is_segment)!;

// The server transaction must come from the native instrumentation API (not the legacy handler),
// The server segment must come from the native instrumentation API (not the legacy handler),
// proving the orchestrion-injected db spans share context with the React Router server span.
expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.react_router.instrumentation_api');
expect(getSpanOp(segmentSpan)).toBe('http.server');
expect(segmentSpan.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');

expect(spans).toContainEqual(
const childSpans = spans.filter(span => !span.is_segment);

expect(childSpans).toContainEqual(
expect.objectContaining({
op: 'db.query',
origin: 'auto.db.redis',
description: 'set test-key [1 other arguments]',
name: 'set test-key [1 other arguments]',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'redis',
'db.operation.name': 'set',
'db.query.text': 'set test-key [1 other arguments]',
attributes: expect.objectContaining({
'sentry.op': { value: 'db.query', type: 'string' },
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
'db.system.name': { value: 'redis', type: 'string' },
'db.operation.name': { value: 'set', type: 'string' },
'db.query.text': { value: 'set test-key [1 other arguments]', type: 'string' },
}),
}),
);
expect(spans).toContainEqual(
expect(childSpans).toContainEqual(
expect.objectContaining({
op: 'db.query',
origin: 'auto.db.redis',
description: 'get test-key',
name: 'get test-key',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'redis',
'db.operation.name': 'get',
'db.query.text': 'get test-key',
attributes: expect.objectContaining({
'sentry.op': { value: 'db.query', type: 'string' },
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
'db.system.name': { value: 'redis', type: 'string' },
'db.operation.name': { value: 'get', type: 'string' },
'db.query.text': { value: 'get test-key', type: 'string' },
}),
}),
);

// Each command maps to exactly one span (no offline-queue duplicate).
const setSpans = spans.filter(span => span.description === 'set test-key [1 other arguments]');
const setSpans = spans.filter(span => span.name === 'set test-key [1 other arguments]');
expect(setSpans).toHaveLength(1);

// Every db span nests under the native instrumentation-API http.server transaction.
const rootSpanId = transactionEvent.contexts?.trace?.span_id;
const spanIds = new Set([rootSpanId, ...spans.map(span => span.span_id)]);
const dbSpans = spans.filter(span => span.origin === 'auto.db.redis');
// Every db span nests under the native instrumentation-API http.server segment.
const spanIds = new Set(spans.filter(span => span.trace_id === segmentSpan.trace_id).map(span => span.span_id));
const dbSpans = spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.db.redis');
expect(dbSpans.every(span => typeof span.parent_span_id === 'string' && spanIds.has(span.parent_span_id))).toBe(
true,
);
});

// Under span streaming the mysql span name is the query summary, so both queries below are named
// `SELECT`. `db.query.text` is what tells them apart.
test('instruments mysql automatically via orchestrion', async ({ page }) => {
const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /performance/db-mysql'
);
});
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/db-mysql' && span.is_segment),
);

await page.goto('/performance/db-mysql');

const transactionEvent = await transactionEventPromise;
const spans = transactionEvent.spans || [];
const spans = await spansPromise;

expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.mysql',
description: 'SELECT 1 + 1 AS solution',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'mysql',
'db.query.text': 'SELECT 1 + 1 AS solution',
'db.user': 'root',
'db.connection_string': expect.any(String),
'server.address': expect.any(String),
'server.port': 3306,
for (const queryText of ['SELECT 1 + 1 AS solution', 'SELECT NOW()']) {
expect(spans).toContainEqual(
expect.objectContaining({
name: 'SELECT',
status: 'ok',
attributes: expect.objectContaining({
'sentry.op': { value: 'db', type: 'string' },
'sentry.origin': { value: 'auto.db.mysql', type: 'string' },
'db.system.name': { value: 'mysql', type: 'string' },
'db.query.text': { value: queryText, type: 'string' },
'db.user': { value: 'root', type: 'string' },
'db.connection_string': { value: expect.any(String), type: 'string' },
'server.address': { value: expect.any(String), type: 'string' },
'server.port': { value: 3306, type: 'integer' },
}),
}),
}),
);
expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.mysql',
description: 'SELECT NOW()',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'mysql',
'db.query.text': 'SELECT NOW()',
'db.user': 'root',
'db.connection_string': expect.any(String),
'server.address': expect.any(String),
'server.port': 3306,
}),
}),
);
);
}
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,6 @@ import { HydratedRouter } from 'react-router/dom';
const tracing = Sentry.reactRouterTracingIntegration({ useInstrumentationAPI: true });

Sentry.init({
traceLifecycle: 'static',
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: 'https://username@domain/123',
tunnel: `http://localhost:3031/`, // proxy server
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
import * as Sentry from '@sentry/react-router';

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://username@domain/123',
environment: 'qa', // dynamic sampling bias to keep transactions
tracesSampleRate: 1.0,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

test.describe('server - instrumentation API error capture', () => {
Expand All@@ -8,15 +8,15 @@ test.describe('server - instrumentation API error capture', () => {
return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-loader' && span.is_segment;
});

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

// Verify the error was captured with correct mechanism and transaction name
expect(error).toMatchObject({
Expand All@@ -36,58 +36,51 @@ test.describe('server - instrumentation API error capture', () => {
});

// Verify the transaction was also created with correct attributes
expect(transaction).toMatchObject({
transaction: 'GET /performance/error-loader',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('GET /performance/error-loader');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});

test('should include loader span in transaction even when loader throws', async ({ page }) => {
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
});
test('should include loader span in the segment even when loader throws', async ({ page }) => {
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/error-loader' && span.is_segment),
);

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const transaction = await txPromise;
const spans = await spansPromise;

// Find the loader span
const loaderSpan = transaction?.spans?.find(span => span.data?.['code.function.name'] === 'loader');
const loaderSpan = spans.find(span => span.attributes['code.function.name']?.value === 'loader');

expect(loaderSpan).toMatchObject({
data: {
'sentry.origin': 'auto.function.react_router.instrumentation_api',
'sentry.op': 'function',
'code.function.name': 'loader',
},
op: 'function',
expect(loaderSpan).toBeDefined();
expect(getSpanOp(loaderSpan!)).toBe('function');
expect(loaderSpan!.attributes).toMatchObject({
'sentry.origin': { value: 'auto.function.react_router.instrumentation_api', type: 'string' },
'sentry.op': { value: 'function', type: 'string' },
'code.function.name': { value: 'loader', type: 'string' },
});
});

test('error and transaction should share the same trace', async ({ page }) => {
test('error and segment span should share the same trace', async ({ page }) => {
const errorPromise = waitForError(APP_NAME, async errorEvent => {
return errorEvent.exception?.values?.[0]?.value === 'Loader error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-loader';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-loader' && span.is_segment;
});

await page.goto(`/performance/error-loader`).catch(() => {
// Expected to fail due to loader error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

// Error and transaction should have the same trace_id
expect(error.contexts?.trace?.trace_id).toBe(transaction.contexts?.trace?.trace_id);
// Error and segment span should have the same trace_id
expect(error.contexts?.trace?.trace_id).toBe(span.trace_id);
});

// Skipped in dev: the action error is sometimes captured via the client instrumentation path
Expand All@@ -101,14 +94,14 @@ test.describe('server - instrumentation API error capture', () => {
return errorEvent.exception?.values?.[0]?.value === 'Action error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'POST /performance/error-action';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'POST /performance/error-action' && span.is_segment;
});

await page.goto(`/performance/error-action`);
await page.getByRole('button', { name: 'Trigger Error' }).click();

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

expect(error).toMatchObject({
exception: {
Expand All@@ -126,31 +119,25 @@ test.describe('server - instrumentation API error capture', () => {
transaction: 'POST /performance/error-action',
});

expect(transaction).toMatchObject({
transaction: 'POST /performance/error-action',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('POST /performance/error-action');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});

test('should capture middleware errors with instrumentation API mechanism', async ({ page }) => {
const errorPromise = waitForError(APP_NAME, async errorEvent => {
return errorEvent.exception?.values?.[0]?.value === 'Middleware error for testing';
});

const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/error-middleware';
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
return span.name === 'GET /performance/error-middleware' && span.is_segment;
});

await page.goto(`/performance/error-middleware`).catch(() => {
// Expected to fail due to middleware error
});

const [error, transaction] = await Promise.all([errorPromise, txPromise]);
const [error, span] = await Promise.all([errorPromise, spanPromise]);

expect(error).toMatchObject({
exception: {
Expand All@@ -168,14 +155,8 @@ test.describe('server - instrumentation API error capture', () => {
transaction: 'GET /performance/error-middleware',
});

expect(transaction).toMatchObject({
transaction: 'GET /performance/error-middleware',
contexts: {
trace: {
op: 'http.server',
origin: 'auto.http.react_router.instrumentation_api',
},
},
});
expect(span.name).toBe('GET /performance/error-middleware');
expect(getSpanOp(span)).toBe('http.server');
expect(span.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');
});
});
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,94 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

// Same spans in both runs, from two injectors: the build-time transform in the server bundle, and
// the runtime hook in `react-router dev`, where the drivers stay on Node's own loader.
test.describe('server - orchestrion db instrumentation', () => {
test('instruments ioredis automatically via orchestrion', async ({ page }) => {
const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /performance/db-ioredis'
);
});
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/db-ioredis' && span.is_segment),
);

await page.goto('/performance/db-ioredis');

const transactionEvent = await transactionEventPromise;
const spans = transactionEvent.spans || [];
const spans = await spansPromise;
const segmentSpan = spans.find(span => span.name === 'GET /performance/db-ioredis' && span.is_segment)!;

// The server transaction must come from the native instrumentation API (not the legacy handler),
// The server segment must come from the native instrumentation API (not the legacy handler),
// proving the orchestrion-injected db spans share context with the React Router server span.
expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.react_router.instrumentation_api');
expect(getSpanOp(segmentSpan)).toBe('http.server');
expect(segmentSpan.attributes['sentry.origin']?.value).toBe('auto.http.react_router.instrumentation_api');

expect(spans).toContainEqual(
const childSpans = spans.filter(span => !span.is_segment);

expect(childSpans).toContainEqual(
expect.objectContaining({
op: 'db.query',
origin: 'auto.db.redis',
description: 'set test-key [1 other arguments]',
name: 'set test-key [1 other arguments]',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'redis',
'db.operation.name': 'set',
'db.query.text': 'set test-key [1 other arguments]',
attributes: expect.objectContaining({
'sentry.op': { value: 'db.query', type: 'string' },
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
'db.system.name': { value: 'redis', type: 'string' },
'db.operation.name': { value: 'set', type: 'string' },
'db.query.text': { value: 'set test-key [1 other arguments]', type: 'string' },
}),
}),
);
expect(spans).toContainEqual(
expect(childSpans).toContainEqual(
expect.objectContaining({
op: 'db.query',
origin: 'auto.db.redis',
description: 'get test-key',
name: 'get test-key',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'redis',
'db.operation.name': 'get',
'db.query.text': 'get test-key',
attributes: expect.objectContaining({
'sentry.op': { value: 'db.query', type: 'string' },
'sentry.origin': { value: 'auto.db.redis', type: 'string' },
'db.system.name': { value: 'redis', type: 'string' },
'db.operation.name': { value: 'get', type: 'string' },
'db.query.text': { value: 'get test-key', type: 'string' },
}),
}),
);

// Each command maps to exactly one span (no offline-queue duplicate).
const setSpans = spans.filter(span => span.description === 'set test-key [1 other arguments]');
const setSpans = spans.filter(span => span.name === 'set test-key [1 other arguments]');
expect(setSpans).toHaveLength(1);

// Every db span nests under the native instrumentation-API http.server transaction.
const rootSpanId = transactionEvent.contexts?.trace?.span_id;
const spanIds = new Set([rootSpanId, ...spans.map(span => span.span_id)]);
const dbSpans = spans.filter(span => span.origin === 'auto.db.redis');
// Every db span nests under the native instrumentation-API http.server segment.
const spanIds = new Set(spans.filter(span => span.trace_id === segmentSpan.trace_id).map(span => span.span_id));
const dbSpans = spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.db.redis');
expect(dbSpans.every(span => typeof span.parent_span_id === 'string' && spanIds.has(span.parent_span_id))).toBe(
true,
);
});

// Under span streaming the mysql span name is the query summary, so both queries below are named
// `SELECT`. `db.query.text` is what tells them apart.
test('instruments mysql automatically via orchestrion', async ({ page }) => {
const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /performance/db-mysql'
);
});
const spansPromise = collectStreamedSpans(APP_NAME, spansOfTrace =>
spansOfTrace.some(span => span.name === 'GET /performance/db-mysql' && span.is_segment),
);

await page.goto('/performance/db-mysql');

const transactionEvent = await transactionEventPromise;
const spans = transactionEvent.spans || [];
const spans = await spansPromise;

expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.mysql',
description: 'SELECT 1 + 1 AS solution',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'mysql',
'db.query.text': 'SELECT 1 + 1 AS solution',
'db.user': 'root',
'db.connection_string': expect.any(String),
'server.address': expect.any(String),
'server.port': 3306,
for (const queryText of ['SELECT 1 + 1 AS solution', 'SELECT NOW()']) {
expect(spans).toContainEqual(
expect.objectContaining({
name: 'SELECT',
status: 'ok',
attributes: expect.objectContaining({
'sentry.op': { value: 'db', type: 'string' },
'sentry.origin': { value: 'auto.db.mysql', type: 'string' },
'db.system.name': { value: 'mysql', type: 'string' },
'db.query.text': { value: queryText, type: 'string' },
'db.user': { value: 'root', type: 'string' },
'db.connection_string': { value: expect.any(String), type: 'string' },
'server.address': { value: expect.any(String), type: 'string' },
'server.port': { value: 3306, type: 'integer' },
}),
}),
}),
);
expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.mysql',
description: 'SELECT NOW()',
status: 'ok',
data: expect.objectContaining({
'db.system.name': 'mysql',
'db.query.text': 'SELECT NOW()',
'db.user': 'root',
'db.connection_string': expect.any(String),
'server.address': expect.any(String),
'server.port': 3306,
}),
}),
);
);
}
});
});
Loading
Loading