Skip to content

Commit 48e03dd

Browse files
committed
Enhance e2e tests
1 parent 003357d commit 48e03dd

File tree

12 files changed

+765
-128
lines changed

12 files changed

+765
-128
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { ActionFunctionArgs, json, LoaderFunctionArgs } from '@remix-run/node';
2+
import { Form, useActionData, useLoaderData } from '@remix-run/react';
3+
import * as Sentry from '@sentry/remix';
4+
5+
// Route with both loader and action to test POST request Server-Timing
6+
7+
export const loader = async ({ request }: LoaderFunctionArgs) => {
8+
const url = new URL(request.url);
9+
const tag = url.searchParams.get('tag');
10+
11+
if (tag) {
12+
Sentry.setTag('sentry_test', tag);
13+
}
14+
15+
return json({ tag });
16+
};
17+
18+
export const action = async ({ request }: ActionFunctionArgs) => {
19+
const formData = await request.formData();
20+
const name = formData.get('name');
21+
const tag = formData.get('tag');
22+
23+
if (tag) {
24+
Sentry.setTag('sentry_test', tag as string);
25+
}
26+
27+
// Simulate some processing
28+
await new Promise(resolve => setTimeout(resolve, 10));
29+
30+
return json({
31+
success: true,
32+
message: `Hello, ${name}!`,
33+
tag,
34+
});
35+
};
36+
37+
export default function ActionTest() {
38+
const { tag } = useLoaderData<typeof loader>();
39+
const actionData = useActionData<typeof action>();
40+
41+
return (
42+
<div style={{ fontFamily: 'system-ui, sans-serif', lineHeight: '1.8' }}>
43+
<h1>Action Test Route</h1>
44+
<p>This route tests Server-Timing headers on POST (action) requests.</p>
45+
46+
<Form method="post">
47+
<input type="hidden" name="tag" value={tag || ''} />
48+
<label>
49+
Name: <input type="text" name="name" defaultValue="Test User" />
50+
</label>
51+
<button type="submit">Submit</button>
52+
</Form>
53+
54+
{actionData && (
55+
<div style={{ marginTop: '20px', padding: '10px', background: '#f0f0f0' }}>
56+
<p>Response: {actionData.message}</p>
57+
<p>Success: {actionData.success ? 'Yes' : 'No'}</p>
58+
</div>
59+
)}
60+
</div>
61+
);
62+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { json, LoaderFunctionArgs } from '@remix-run/node';
2+
import * as Sentry from '@sentry/remix';
3+
4+
// Resource route - no default export, just returns JSON
5+
// This is commonly used for API endpoints in Remix
6+
7+
export const loader = async ({ request }: LoaderFunctionArgs) => {
8+
const url = new URL(request.url);
9+
const tag = url.searchParams.get('tag');
10+
11+
if (tag) {
12+
Sentry.setTag('sentry_test', tag);
13+
}
14+
15+
return json({
16+
success: true,
17+
data: {
18+
message: 'Hello from resource route!',
19+
timestamp: Date.now(),
20+
},
21+
});
22+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { LoaderFunctionArgs } from '@remix-run/node';
2+
import * as Sentry from '@sentry/remix';
3+
4+
export const loader = async ({ request }: LoaderFunctionArgs) => {
5+
const url = new URL(request.url);
6+
const tag = url.searchParams.get('tag');
7+
8+
if (tag) {
9+
Sentry.setTag('sentry_test', tag);
10+
}
11+
12+
// Throw an error to test 500 response handling
13+
throw new Error('Test error for Server-Timing propagation');
14+
};
15+
16+
export default function ErrorTest() {
17+
return (
18+
<div>
19+
<h1>Error Test Route</h1>
20+
<p>This should not render - the loader throws an error.</p>
21+
</div>
22+
);
23+
}
24+
25+
export function ErrorBoundary() {
26+
return (
27+
<div>
28+
<h1>Error Occurred</h1>
29+
<p>An error was thrown in the loader.</p>
30+
</div>
31+
);
32+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { HeadersFunction, json, LoaderFunctionArgs } from '@remix-run/node';
2+
import * as Sentry from '@sentry/remix';
3+
4+
// Export headers function to include loader headers in document responses
5+
// See: https://remix.run/docs/en/main/route/headers
6+
export const headers: HeadersFunction = ({ loaderHeaders }) => {
7+
return loaderHeaders;
8+
};
9+
10+
export const loader = async ({ request }: LoaderFunctionArgs) => {
11+
const url = new URL(request.url);
12+
const tag = url.searchParams.get('tag');
13+
14+
if (tag) {
15+
Sentry.setTag('sentry_test', tag);
16+
}
17+
18+
return json(
19+
{ message: 'Merge test route' },
20+
{
21+
headers: {
22+
// Set an existing Server-Timing header that should be merged with Sentry's
23+
'Server-Timing': 'db;dur=53.2, cache;desc="Cache Read";dur=1.5',
24+
},
25+
},
26+
);
27+
};
28+
29+
export default function MergeTest() {
30+
return (
31+
<div style={{ fontFamily: 'system-ui, sans-serif', lineHeight: '1.8' }}>
32+
<h1>Server-Timing Merge Test</h1>
33+
<p>This route sets an existing Server-Timing header to test merging.</p>
34+
<p>
35+
Expected: db;dur=53.2, cache;desc=&quot;Cache Read&quot;;dur=1.5, sentry-trace;desc=&quot;...&quot;,
36+
baggage;desc=&quot;...&quot;
37+
</p>
38+
</div>
39+
);
40+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { json, LoaderFunctionArgs } from '@remix-run/node';
2+
import { useLoaderData } from '@remix-run/react';
3+
import * as Sentry from '@sentry/remix';
4+
5+
// Child route loader - runs after parent loader
6+
export const loader = async ({ request }: LoaderFunctionArgs) => {
7+
const url = new URL(request.url);
8+
const tag = url.searchParams.get('tag');
9+
10+
if (tag) {
11+
Sentry.setTag('sentry_test', tag);
12+
}
13+
14+
// Get current trace info for debugging
15+
const span = Sentry.getActiveSpan();
16+
const rootSpan = span ? Sentry.getRootSpan(span) : null;
17+
const traceId = rootSpan ? Sentry.spanToJSON(rootSpan).trace_id : 'no-trace';
18+
19+
// Simulate some async work
20+
await new Promise(resolve => setTimeout(resolve, 5));
21+
22+
return json({
23+
childData: 'Child loader data',
24+
childTraceId: traceId,
25+
});
26+
};
27+
28+
export default function Child() {
29+
const { childData, childTraceId } = useLoaderData<typeof loader>();
30+
31+
return (
32+
<div>
33+
<h2>Child Route</h2>
34+
<p>Child data: {childData}</p>
35+
<p>
36+
Child trace ID: <code>{childTraceId}</code>
37+
</p>
38+
</div>
39+
);
40+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { json, LoaderFunctionArgs } from '@remix-run/node';
2+
import { Outlet, useLoaderData } from '@remix-run/react';
3+
import * as Sentry from '@sentry/remix';
4+
5+
// Parent route loader - runs before child loaders
6+
export const loader = async ({ request }: LoaderFunctionArgs) => {
7+
const url = new URL(request.url);
8+
const tag = url.searchParams.get('tag');
9+
10+
if (tag) {
11+
Sentry.setTag('sentry_test', tag);
12+
}
13+
14+
// Get current trace info for debugging
15+
const span = Sentry.getActiveSpan();
16+
const rootSpan = span ? Sentry.getRootSpan(span) : null;
17+
const traceId = rootSpan ? Sentry.spanToJSON(rootSpan).trace_id : 'no-trace';
18+
19+
// Simulate some async work
20+
await new Promise(resolve => setTimeout(resolve, 5));
21+
22+
return json({
23+
parentData: 'Parent loader data',
24+
parentTraceId: traceId,
25+
});
26+
};
27+
28+
export default function Parent() {
29+
const { parentData, parentTraceId } = useLoaderData<typeof loader>();
30+
31+
return (
32+
<div style={{ fontFamily: 'system-ui, sans-serif', lineHeight: '1.8' }}>
33+
<h1>Parent Route</h1>
34+
<p>Parent data: {parentData}</p>
35+
<p>
36+
Parent trace ID: <code>{parentTraceId}</code>
37+
</p>
38+
<div style={{ border: '1px solid #ccc', padding: '10px', marginTop: '10px' }}>
39+
<Outlet />
40+
</div>
41+
</div>
42+
);
43+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { json, LoaderFunctionArgs } from '@remix-run/node';
2+
import { Link, useLoaderData } from '@remix-run/react';
3+
import * as Sentry from '@sentry/remix';
4+
5+
export const loader = async ({ request }: LoaderFunctionArgs) => {
6+
const url = new URL(request.url);
7+
const tag = url.searchParams.get('tag');
8+
9+
if (tag) {
10+
Sentry.setTag('sentry_test', tag);
11+
}
12+
13+
return json({ tag });
14+
};
15+
16+
export default function PrefetchTest() {
17+
const { tag } = useLoaderData<typeof loader>();
18+
19+
return (
20+
<div style={{ fontFamily: 'system-ui, sans-serif', lineHeight: '1.8' }}>
21+
<h1>Prefetch Test</h1>
22+
<p>This page tests Server-Timing with prefetch behavior.</p>
23+
24+
<h3>Links with different prefetch modes:</h3>
25+
<ul>
26+
<li>
27+
<Link id="prefetch-intent" to={`/user/prefetch-target?tag=${tag}`} prefetch="intent">
28+
Prefetch on Intent (hover)
29+
</Link>
30+
</li>
31+
<li>
32+
<Link id="prefetch-render" to={`/user/prefetch-target2?tag=${tag}`} prefetch="render">
33+
Prefetch on Render
34+
</Link>
35+
</li>
36+
<li>
37+
<Link id="prefetch-none" to={`/user/prefetch-target3?tag=${tag}`} prefetch="none">
38+
No Prefetch
39+
</Link>
40+
</li>
41+
</ul>
42+
</div>
43+
);
44+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { LoaderFunctionArgs, redirect } from '@remix-run/node';
2+
import * as Sentry from '@sentry/remix';
3+
4+
// Route that returns a redirect response
5+
// Tests that Server-Timing headers are present on redirect responses
6+
7+
export const loader = async ({ request }: LoaderFunctionArgs) => {
8+
const url = new URL(request.url);
9+
const tag = url.searchParams.get('tag');
10+
11+
if (tag) {
12+
Sentry.setTag('sentry_test', tag);
13+
}
14+
15+
// Redirect to user page, preserving the tag
16+
const targetUrl = tag ? `/user/redirected?tag=${tag}` : '/user/redirected';
17+
return redirect(targetUrl);
18+
};
19+
20+
// No default export needed - loader always redirects
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { defer, LoaderFunctionArgs } from '@remix-run/node';
2+
import { Await, useLoaderData } from '@remix-run/react';
3+
import * as Sentry from '@sentry/remix';
4+
import { Suspense } from 'react';
5+
6+
// Simulate a slow async operation
7+
async function getSlowData(): Promise<{ message: string; timestamp: number }> {
8+
await new Promise(resolve => setTimeout(resolve, 100));
9+
return {
10+
message: 'Deferred data loaded (legacy)!',
11+
timestamp: Date.now(),
12+
};
13+
}
14+
15+
// Legacy streaming using defer() - deprecated but still supported
16+
export const loader = async ({ request }: LoaderFunctionArgs) => {
17+
const url = new URL(request.url);
18+
const tag = url.searchParams.get('tag');
19+
20+
if (tag) {
21+
Sentry.setTag('sentry_test', tag);
22+
}
23+
24+
// Legacy pattern: use defer() explicitly
25+
return defer({
26+
immediate: { greeting: 'Hello from legacy streaming route!' },
27+
deferred: getSlowData(),
28+
});
29+
};
30+
31+
export default function StreamingLegacy() {
32+
const { immediate, deferred } = useLoaderData<typeof loader>();
33+
34+
return (
35+
<div style={{ fontFamily: 'system-ui, sans-serif', lineHeight: '1.8' }}>
36+
<h1>Legacy Streaming Response Test (defer)</h1>
37+
<p>Immediate data: {immediate.greeting}</p>
38+
39+
<Suspense fallback={<p>Loading deferred data...</p>}>
40+
<Await resolve={deferred}>
41+
{data => (
42+
<div>
43+
<p>Deferred message: {data.message}</p>
44+
<p>Timestamp: {data.timestamp}</p>
45+
</div>
46+
)}
47+
</Await>
48+
</Suspense>
49+
</div>
50+
);
51+
}

0 commit comments

Comments
 (0)