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 @@ -5,7 +5,6 @@ import { useEffect } 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
Expand Up @@ -9,7 +9,6 @@ import * as build from '../build/server';
export const onRequest = [
(context: EventPluginContext<any, any, any, any>) =>
sentryPagesPlugin({
traceLifecycle: 'static',
dsn: context.env.E2E_TEST_DSN,
tracesSampleRate: 1.0,
})(context),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,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 Remix have hydrated.
// The pageload span only completes once the client SDK and Remix 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('remix-hydrogen', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/';
const pageloadSpanPromise = waitForStreamedSpan('remix-hydrogen', span => {
return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/';
});

const errorPromise = waitForError('remix-hydrogen', 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 Down
Original file line number Diff line number Diff line change
@@ -1,67 +1,50 @@
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('remix-hydrogen', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/';
test('Sends a pageload span to Sentry', async ({ page }) => {
const spanPromise = waitForStreamedSpan('remix-hydrogen', span => {
return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/';
});

await page.goto('/');

const transactionEvent = await transactionPromise;

expect(transactionEvent).toBeDefined();
expect(transactionEvent).toMatchObject({
transaction: '/',
contexts: {
trace: {
data: {
'sentry.segment.name.source': 'route',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/),
'url.path': '/',
'url.template': '/',
},
},
},
const span = await spanPromise;

expect(span.attributes).toMatchObject({
'sentry.segment.name.source': { value: 'route', type: 'string' },
'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), type: 'string' },
'url.path': { value: '/', type: 'string' },
'url.template': { value: '/', type: 'string' },
});
});

test('Sends a navigation transaction to Sentry', async ({ page }) => {
// Wait for the initial pageload transaction first. This ensures the client SDK and
// Remix 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 client-side `navigation` one,
// which makes this test flaky.
const pageloadTransactionPromise = waitForTransaction('remix-hydrogen', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/';
test('Sends a navigation span to Sentry', async ({ page }) => {
// Wait for the initial pageload span first. This ensures the client SDK and Remix 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` span) instead of a
// client-side `navigation` one, which makes this test flaky.
const pageloadSpanPromise = waitForStreamedSpan('remix-hydrogen', span => {
return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/';
});

const transactionPromise = waitForTransaction('remix-hydrogen', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === '/user/:id';
const spanPromise = waitForStreamedSpan('remix-hydrogen', 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;

expect(transactionEvent).toBeDefined();
expect(transactionEvent).toMatchObject({
transaction: '/user/:id',
contexts: {
trace: {
data: {
'sentry.segment.name.source': 'route',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/),
'url.path': '/user/5',
'url.template': '/user/:id',
},
},
},
const span = await spanPromise;

expect(span.attributes).toMatchObject({
'sentry.segment.name.source': { value: 'route', type: 'string' },
'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), type: 'string' },
'url.path': { value: '/user/5', type: 'string' },
'url.template': { value: '/user/:id', type: 'string' },
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,53 +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 = 'remix-hydrogen';

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

test('Sends parameterized transaction name to Sentry', async ({ page }) => {
const transactionPromise = waitForTransaction('remix-hydrogen', 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();
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('remix-hydrogen', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag;
});

const pageLoadTransactionPromise = waitForTransaction('remix-hydrogen', 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