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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,30 +44,30 @@ test('Sends a navigation transaction with parameterized route to Sentry', async
expect(transactionEvent.transaction).toBeTruthy();
});

test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => {
test('Server-Timing header contains sentry-trace and baggage for the root route', async ({ page }) => {
const responsePromise = page.waitForResponse(response => response.url().endsWith('/') && response.status() === 200);

await page.goto('/');

const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', {
state: 'attached',
});
const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', {
state: 'attached',
});
const response = await responsePromise;
const serverTimingHeader = response.headers()['server-timing'];

expect(sentryTraceMetaTag).toBeTruthy();
expect(baggageMetaTag).toBeTruthy();
expect(serverTimingHeader).toBeDefined();
expect(serverTimingHeader).toContain('sentry-trace');
expect(serverTimingHeader).toContain('baggage');
});

test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => {
test('Server-Timing header contains sentry-trace and baggage for a sub-route', async ({ page }) => {
const responsePromise = page.waitForResponse(
response => response.url().includes('/user/123') && response.status() === 200,
);

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

const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', {
state: 'attached',
});
const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', {
state: 'attached',
});
const response = await responsePromise;
const serverTimingHeader = response.headers()['server-timing'];

expect(sentryTraceMetaTag).toBeTruthy();
expect(baggageMetaTag).toBeTruthy();
expect(serverTimingHeader).toBeDefined();
expect(serverTimingHeader).toContain('sentry-trace');
expect(serverTimingHeader).toContain('baggage');
});
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page
expect(httpServerSpanId).toBeDefined();

expect(pageLoadTraceId).toEqual(httpServerTraceId);
expect(pageLoadParentSpanId).toEqual(loaderSpanId);
expect(pageLoadParentSpanId).toEqual(httpServerSpanId);
expect(pageLoadSpanId).not.toEqual(httpServerSpanId);
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,30 @@ test('Sends a navigation transaction to Sentry', async ({ page }) => {
expect(transactionEvent).toBeDefined();
});

test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => {
test('Server-Timing header contains sentry-trace and baggage for the root route', async ({ page }) => {
const responsePromise = page.waitForResponse(response => response.url().endsWith('/') && response.status() === 200);

await page.goto('/');

const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', {
state: 'attached',
});
const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', {
state: 'attached',
});
const response = await responsePromise;
const serverTimingHeader = response.headers()['server-timing'];

expect(sentryTraceMetaTag).toBeTruthy();
expect(baggageMetaTag).toBeTruthy();
expect(serverTimingHeader).toBeDefined();
expect(serverTimingHeader).toContain('sentry-trace');
expect(serverTimingHeader).toContain('baggage');
});

test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => {
test('Server-Timing header contains sentry-trace and baggage for a sub-route', async ({ page }) => {
const responsePromise = page.waitForResponse(
response => response.url().includes('/user/123') && response.status() === 200,
);

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

const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', {
state: 'attached',
});
const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', {
state: 'attached',
});
const response = await responsePromise;
const serverTimingHeader = response.headers()['server-timing'];

expect(sentryTraceMetaTag).toBeTruthy();
expect(baggageMetaTag).toBeTruthy();
expect(serverTimingHeader).toBeDefined();
expect(serverTimingHeader).toContain('sentry-trace');
expect(serverTimingHeader).toContain('baggage');
});
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ test('Propagates trace when ErrorBoundary is triggered', async ({ page }) => {
expect(httpServerSpanId).toBeDefined();

expect(pageLoadTraceId).toEqual(httpServerTraceId);
expect(pageLoadParentSpanId).toEqual(loaderSpanId);
expect(pageLoadParentSpanId).toEqual(httpServerSpanId);
expect(pageLoadSpanId).not.toEqual(httpServerSpanId);
});

Expand Down Expand Up @@ -139,6 +139,6 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page

expect(loaderParentSpanId).toEqual(httpServerSpanId);
expect(pageLoadTraceId).toEqual(httpServerTraceId);
expect(pageLoadParentSpanId).toEqual(loaderSpanId);
expect(pageLoadParentSpanId).toEqual(httpServerSpanId);
expect(pageLoadSpanId).not.toEqual(httpServerSpanId);
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,30 @@ test('Sends a navigation transaction to Sentry', async ({ page }) => {
expect(transactionEvent).toBeDefined();
});

test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => {
test('Server-Timing header contains sentry-trace and baggage for the root route', async ({ page }) => {
const responsePromise = page.waitForResponse(response => response.url().endsWith('/') && response.status() === 200);

await page.goto('/');

const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', {
state: 'attached',
});
const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', {
state: 'attached',
});
const response = await responsePromise;
const serverTimingHeader = response.headers()['server-timing'];

expect(sentryTraceMetaTag).toBeTruthy();
expect(baggageMetaTag).toBeTruthy();
expect(serverTimingHeader).toBeDefined();
expect(serverTimingHeader).toContain('sentry-trace');
expect(serverTimingHeader).toContain('baggage');
});

test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => {
test('Server-Timing header contains sentry-trace and baggage for a sub-route', async ({ page }) => {
const responsePromise = page.waitForResponse(
response => response.url().includes('/user/123') && response.status() === 200,
);

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

const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', {
state: 'attached',
});
const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', {
state: 'attached',
});
const response = await responsePromise;
const serverTimingHeader = response.headers()['server-timing'];

expect(sentryTraceMetaTag).toBeTruthy();
expect(baggageMetaTag).toBeTruthy();
expect(serverTimingHeader).toBeDefined();
expect(serverTimingHeader).toContain('sentry-trace');
expect(serverTimingHeader).toContain('baggage');
});
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,6 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page
expect(httpServerSpanId).toBeDefined();

expect(pageLoadTraceId).toEqual(httpServerTraceId);
expect(pageLoadParentSpanId).toEqual(loaderSpanId);
expect(pageLoadParentSpanId).toEqual(httpServerSpanId);
expect(pageLoadSpanId).not.toEqual(httpServerSpanId);
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,30 @@ test('Sends a navigation transaction to Sentry', async ({ page }) => {
expect(transactionEvent).toBeDefined();
});

test('Renders `sentry-trace` and `baggage` meta tags for the root route', async ({ page }) => {
test('Server-Timing header contains sentry-trace and baggage for the root route', async ({ page }) => {
const responsePromise = page.waitForResponse(response => response.url().endsWith('/') && response.status() === 200);

await page.goto('/');

const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', {
state: 'attached',
});
const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', {
state: 'attached',
});
const response = await responsePromise;
const serverTimingHeader = response.headers()['server-timing'];

expect(sentryTraceMetaTag).toBeTruthy();
expect(baggageMetaTag).toBeTruthy();
expect(serverTimingHeader).toBeDefined();
expect(serverTimingHeader).toContain('sentry-trace');
expect(serverTimingHeader).toContain('baggage');
});

test('Renders `sentry-trace` and `baggage` meta tags for a sub-route', async ({ page }) => {
test('Server-Timing header contains sentry-trace and baggage for a sub-route', async ({ page }) => {
const responsePromise = page.waitForResponse(
response => response.url().includes('/user/123') && response.status() === 200,
);

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

const sentryTraceMetaTag = await page.waitForSelector('meta[name="sentry-trace"]', {
state: 'attached',
});
const baggageMetaTag = await page.waitForSelector('meta[name="baggage"]', {
state: 'attached',
});
const response = await responsePromise;
const serverTimingHeader = response.headers()['server-timing'];

expect(sentryTraceMetaTag).toBeTruthy();
expect(baggageMetaTag).toBeTruthy();
expect(serverTimingHeader).toBeDefined();
expect(serverTimingHeader).toContain('sentry-trace');
expect(serverTimingHeader).toContain('baggage');
});
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,6 @@ test('Sends two linked transactions (server & client) to Sentry', async ({ page
expect(httpServerSpanId).toBeDefined();

expect(pageLoadTraceId).toEqual(httpServerTraceId);
expect(pageLoadParentSpanId).toEqual(loaderSpanId);
expect(pageLoadParentSpanId).toEqual(httpServerSpanId);
expect(pageLoadSpanId).not.toEqual(httpServerSpanId);
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { RemixServer } from '@remix-run/react';
import { addSentryServerTimingHeader } from '@sentry/remix/cloudflare';
import { createContentSecurityPolicy } from '@shopify/hydrogen';
import type { EntryContext } from '@shopify/remix-oxygen';
import isbot from 'isbot';
Expand Down Expand Up @@ -43,8 +44,11 @@ export default async function handleRequest(
// This is required for Sentry's profiling integration
responseHeaders.set('Document-Policy', 'js-profiling');

return new Response(body, {
const response = new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});

// Add Server-Timing header with Sentry trace context for client-side trace propagation
return addSentryServerTimingHeader(response);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { expect, test } from '@playwright/test';

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

test('Server-Timing header contains sentry-trace on page load (Cloudflare)', async ({ page }) => {
// Intercept the document response (not data requests or other resources)
const responsePromise = page.waitForResponse(
response =>
response.url().endsWith('/') && response.status() === 200 && response.request().resourceType() === 'document',
);

await page.goto('/');

const response = await responsePromise;
const serverTimingHeader = response.headers()['server-timing'];

expect(serverTimingHeader).toBeDefined();
expect(serverTimingHeader).toContain('sentry-trace');
expect(serverTimingHeader).toContain('baggage');
});

test('Server-Timing header contains valid trace ID format (Cloudflare)', async ({ page }) => {
// Match only the document response for /user/123 (not .data requests)
const responsePromise = page.waitForResponse(
response =>
response.url().endsWith('/user/123') &&
response.status() === 200 &&
response.request().resourceType() === 'document',
);

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

const response = await responsePromise;
const serverTimingHeader = response.headers()['server-timing'];

expect(serverTimingHeader).toBeDefined();

// Extract sentry-trace value from header
// Format: sentry-trace;desc="traceid-spanid" or sentry-trace;desc="traceid-spanid-sampled"
const sentryTraceMatch = serverTimingHeader.match(/sentry-trace;desc="([^"]+)"/);
expect(sentryTraceMatch).toBeTruthy();

const sentryTraceValue = sentryTraceMatch![1];

// Validate sentry-trace format: traceid-spanid or traceid-spanid-sampled (case insensitive)
// The format is: 32 hex chars, dash, 16 hex chars, optionally followed by dash and 0 or 1
const traceIdMatch = sentryTraceValue.match(/^([a-fA-F0-9]{32})-([a-fA-F0-9]{16})(?:-([01]))?$/);
expect(traceIdMatch).toBeTruthy();

// Verify the trace ID and span ID parts
const [, traceId, spanId] = traceIdMatch!;
expect(traceId).toHaveLength(32);
expect(spanId).toHaveLength(16);
});

test('Server-Timing header is present on parameterized routes (Cloudflare)', async ({ page }) => {
// Match only the document response for /user/456 (not .data requests)
const responsePromise = page.waitForResponse(
response =>
response.url().endsWith('/user/456') &&
response.status() === 200 &&
response.request().resourceType() === 'document',
);

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

const response = await responsePromise;
const serverTimingHeader = response.headers()['server-timing'];

expect(serverTimingHeader).toBeDefined();
expect(serverTimingHeader).toContain('sentry-trace');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: ['@remix-run/eslint-config', '@remix-run/eslint-config/node'],
rules: {
'import/no-unresolved': 'off',
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
build
.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@sentry:registry=http://127.0.0.1:4873
@sentry-internal:registry=http://127.0.0.1:4873
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* By default, Remix will handle hydrating your app on the client for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal`
* For more information, see https://remix.run/file-conventions/entry.client
*/

// Extend the Window interface to include ENV
declare global {
interface Window {
ENV: {
SENTRY_DSN: string;
[key: string]: unknown;
};
}
}

import { RemixBrowser, useLocation, useMatches } from '@remix-run/react';
import * as Sentry from '@sentry/remix';
import { StrictMode, startTransition, useEffect } from 'react';
import { hydrateRoot } from 'react-dom/client';

Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: window.ENV.SENTRY_DSN,
integrations: [
Sentry.browserTracingIntegration({
useEffect,
useLocation,
useMatches,
}),
],
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions
tunnel: 'http://localhost:3031/', // proxy server
});

startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>,
);
});
Loading
Loading