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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { StrictMode, startTransition } from 'react';
import { hydrateRoot } from 'react-dom/client';

Sentry.init({
traceLifecycle: 'static',
environment: 'qa', // dynamic sampling bias to keep transactions
// Could not find a working way to set the DSN in the browser side from the environment variables
dsn: 'https://public@dsn.ingest.sentry.io/1337',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as Sentry from '@sentry/react-router';
Sentry.init({
traceLifecycle: 'static',
dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0',
// Adds request headers and IP for users, for more info visit:
// https://docs.sentry.io/platforms/javascript/guides/react-router/configuration/options/#dataCollection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export default {
return wrapRequestHandler(
{
options: {
traceLifecycle: 'static',
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: 'https://public@dsn.ingest.sentry.io/1337',
tracesSampleRate: 1.0,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
import { getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';

test('Sends a client-side exception to Sentry', async ({ page }) => {
// The pageload transaction only completes once the client SDK and the router have hydrated.
// The pageload span only completes once the client SDK and the router have hydrated.
// Awaiting it before clicking guarantees the button's onClick handler is attached — a click that
// lands before hydration would do nothing, and the exception would never be captured.
const pageloadTransactionPromise = waitForTransaction('hydrogen-react-router-7', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/';
const pageloadSpanPromise = waitForStreamedSpan('hydrogen-react-router-7', span => {
return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/';
});

const errorPromise = waitForError('hydrogen-react-router-7', errorEvent => {
Expand All @@ -15,7 +15,7 @@ test('Sends a client-side exception to Sentry', async ({ page }) => {

await page.goto('/');

await pageloadTransactionPromise;
await pageloadSpanPromise;

const exceptionButton = page.locator('id=exception-button');
await exceptionButton.click();
Expand All @@ -27,8 +27,8 @@ test('Sends a client-side exception to Sentry', async ({ page }) => {

test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) => {
// Wait for hydration (see above) before clicking, so the button's onClick handler is attached.
const pageloadTransactionPromise = waitForTransaction('hydrogen-react-router-7', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/client-error';
const pageloadSpanPromise = waitForStreamedSpan('hydrogen-react-router-7', span => {
return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/client-error';
});

const errorPromise = waitForError('hydrogen-react-router-7', errorEvent => {
Expand All @@ -37,7 +37,7 @@ test('Sends a client-side ErrorBoundary exception to Sentry', async ({ page }) =

await page.goto('/client-error');

await pageloadTransactionPromise;
await pageloadSpanPromise;

const throwButton = page.locator('id=throw-on-click');
await throwButton.click();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,39 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';

test('Sends a pageload transaction to Sentry', async ({ page }) => {
const transactionPromise = waitForTransaction('hydrogen-react-router-7', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/';
test('Sends a pageload span to Sentry', async ({ page }) => {
const spanPromise = waitForStreamedSpan('hydrogen-react-router-7', span => {
return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/';
});

await page.goto('/');

const transactionEvent = await transactionPromise;

expect(transactionEvent).toBeDefined();
expect(await spanPromise).toBeDefined();
});

test('Sends a navigation transaction to Sentry', async ({ page }) => {
// Wait for the initial pageload transaction first. This ensures the client SDK and router are fully
test('Sends a navigation span to Sentry', async ({ page }) => {
// Wait for the initial pageload span first. This ensures the client SDK and router are fully
// hydrated before we click the link. Clicking before hydration completes makes the `<Link>` behave
// like a plain anchor, triggering a full page navigation (a `pageload` transaction) instead of a
// like a plain anchor, triggering a full page navigation (a `pageload` span) instead of a
// client-side `navigation` one, which makes this test flaky.
const pageloadTransactionPromise = waitForTransaction('hydrogen-react-router-7', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/';
const pageloadSpanPromise = waitForStreamedSpan('hydrogen-react-router-7', span => {
return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/';
});

const transactionPromise = waitForTransaction('hydrogen-react-router-7', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === '/user/:id';
const spanPromise = waitForStreamedSpan('hydrogen-react-router-7', span => {
return getSpanOp(span) === 'navigation' && span.is_segment && span.name === '/user/:id';
});

await page.goto('/');

await pageloadTransactionPromise;
await pageloadSpanPromise;

const linkElement = page.locator('id=navigation');
await linkElement.click();

const transactionEvent = await transactionPromise;
const span = await spanPromise;

expect(transactionEvent).toBeDefined();
expect(transactionEvent).toMatchObject({
transaction: '/user/:id',
});
expect(span.name).toBe('/user/:id');
});

test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,54 +1,48 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import type { SerializedStreamedSpan } from '@sentry-internal/test-utils';
import { getSpanOp, waitForStreamedSpan, waitForStreamedSpans } from '@sentry-internal/test-utils';

const APP_NAME = 'hydrogen-react-router-7';

test.describe.configure({ mode: 'serial' });

test('Sends parameterized transaction name to Sentry', async ({ page }) => {
const transactionPromise = waitForTransaction('hydrogen-react-router-7', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'http.server';
test('Sends a parameterized span name to Sentry', async ({ page }) => {
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
// The span name is parameterized (route pattern, not the actual URL).
return getSpanOp(span) === 'http.server' && span.is_segment && span.name === 'GET /user/:id';
});

await page.goto('/user/123');

const transaction = await transactionPromise;
const span = await spanPromise;

expect(transaction).toBeDefined();
// Transaction name should be parameterized (route pattern, not actual URL)
expect(transaction.transaction).toBe('GET /user/:id');
expect(span.attributes['sentry.segment.name.source']?.value).toBe('route');
});

test('Sends two linked transactions (server & client) to Sentry', async ({ page }) => {
// We use this to identify the transactions
const testTag = crypto.randomUUID();

const httpServerTransactionPromise = waitForTransaction('hydrogen-react-router-7', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag;
});

const pageLoadTransactionPromise = waitForTransaction('hydrogen-react-router-7', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.tags?.['sentry_test'] === testTag;
test('Sends two linked spans (server & client) to Sentry', async ({ page }) => {
// Streamed spans are buffered before they flush, so spans from an earlier page load can still be
// arriving here. The document advertises its own trace in the `sentry-trace` meta tag, so that is
// what tells this page load's spans apart rather than the op or the URL.
const streamedSpans: SerializedStreamedSpan[] = [];
void waitForStreamedSpans(APP_NAME, spans => {
streamedSpans.push(...spans);
return false;
});

page.goto(`/?tag=${testTag}`);

const pageloadTransaction = await pageLoadTransactionPromise;
const httpServerTransaction = await httpServerTransactionPromise;

expect(pageloadTransaction).toBeDefined();
expect(httpServerTransaction).toBeDefined();

const httpServerTraceId = httpServerTransaction.contexts?.trace?.trace_id;
const httpServerSpanId = httpServerTransaction.contexts?.trace?.span_id;

const pageLoadTraceId = pageloadTransaction.contexts?.trace?.trace_id;
const pageLoadSpanId = pageloadTransaction.contexts?.trace?.span_id;
await page.goto('/');

expect(httpServerTransaction.transaction).toBe('GET /');
expect(pageloadTransaction.transaction).toBe('/');
const sentryTrace = await page.getAttribute('meta[name="sentry-trace"]', 'content');
const [traceId] = (sentryTrace ?? '').split('-');
expect(traceId).toMatch(/^[a-f0-9]{32}$/);

expect(httpServerTraceId).toBeDefined();
expect(httpServerSpanId).toBeDefined();
const findServerSegmentSpan = () =>
streamedSpans.find(span => getSpanOp(span) === 'http.server' && span.is_segment && span.trace_id === traceId);
await expect.poll(findServerSegmentSpan).toBeDefined();
expect(findServerSegmentSpan()!.name).toBe('GET /');

expect(pageLoadTraceId).toEqual(httpServerTraceId);
expect(pageLoadSpanId).not.toEqual(httpServerSpanId);
const findPageloadSpan = () =>
streamedSpans.find(span => getSpanOp(span) === 'pageload' && span.is_segment && span.trace_id === traceId);
await expect.poll(findPageloadSpan).toBeDefined();
expect(findPageloadSpan()!.name).toBe('/');
expect(findPageloadSpan()!.span_id).not.toBe(findServerSegmentSpan()!.span_id);
});
Loading