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
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,7 @@ jobs:
'sveltekit',
'sveltekit-2',
'sveltekit-2-svelte-5',
'tanstack-router',
'generic-ts3.8',
'node-fastify',
'node-hapi',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
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,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tanstack Example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "tanstack-router-e2e-test-application",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"build": "vite build",
"start": "vite preview",
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
"test:build": "pnpm install && npx playwright install && pnpm build",
"test:assert": "pnpm test"
},
"dependencies": {
"@sentry/react": "latest || *",
"@tanstack/react-router": "1.34.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"typescript": "^5.2.2",
"vite": "^5.2.0",
"@playwright/test": "^1.41.1",
"@sentry-internal/event-proxy-server": "link:../../../event-proxy-server"
},
"volta": {
"extends": "../../package.json"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { PlaywrightTestConfig } from '@playwright/test';
import { devices } from '@playwright/test';

const appPort = 3030;
const eventProxyPort = 3031;

/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: './tests',
/* Maximum time one test can run for. */
timeout: 150_000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: 0,
/* Opt out of parallel tests on CI. */
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'list',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',

baseURL: `http://localhost:${appPort}`,
},

/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
],

/* Run your local dev server before starting the tests */

webServer: [
{
command: 'node start-event-proxy.mjs',
port: eventProxyPort,
},
{
command: 'pnpm start',
port: appPort,
},
],
};

export default config;
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import * as Sentry from '@sentry/react';
import { Link, Outlet, RouterProvider, createRootRoute, createRoute, createRouter } from '@tanstack/react-router';
import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';

const rootRoute = createRootRoute({
component: () => (
<>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/posts/$postId" params={{ postId: '1' }}>
Post 1
</Link>
</li>
<li>
<Link to="/posts/$postId" params={{ postId: '2' }} id="nav-link">
Post 2
</Link>
</li>
</ul>
<hr />
<Outlet />
</>
),
});

const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: function Index() {
return (
<div>
<h3>Welcome Home!</h3>
</div>
);
},
});

const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts/',
});

const postIdRoute = createRoute({
getParentRoute: () => postsRoute,
path: '$postId',
shouldReload() {
return true;
},
loader: ({ params }) => {
return Sentry.startSpan({ name: `loading-post-${params.postId}` }, async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
});
},
component: function Post() {
const { postId } = postIdRoute.useParams();
return <div>Post ID: {postId}</div>;
},
});

const routeTree = rootRoute.addChildren([indexRoute, postsRoute.addChildren([postIdRoute])]);

const router = createRouter({ routeTree });

declare const __APP_DSN__: string;

Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: __APP_DSN__,
integrations: [Sentry.tanstackRouterBrowserTracingIntegration(router)],
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0,
release: 'e2e-test',
tunnel: 'http://localhost:3031/', // proxy server

// Always capture replays, so we can test this properly
replaysSessionSampleRate: 1.0,
replaysOnErrorSampleRate: 0.0,
});

declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}

const rootElement = document.getElementById('root')!;
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { startEventProxyServer } from '@sentry-internal/event-proxy-server';

startEventProxyServer({
port: 3031,
proxyServerName: 'tanstack-router',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/event-proxy-server';

test('sends a pageload transaction with a parameterized URL', async ({ page }) => {
const transactionPromise = waitForTransaction('tanstack-router', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
});

await page.goto(`/posts/456`);

const rootSpan = await transactionPromise;

expect(rootSpan).toMatchObject({
contexts: {
trace: {
data: {
'sentry.source': 'route',
'sentry.origin': 'auto.pageload.react.tanstack_router',
'sentry.op': 'pageload',
'url.path.params.postId': '456',
},
op: 'pageload',
origin: 'auto.pageload.react.tanstack_router',
},
},
transaction: '/posts/$postId',
transaction_info: {
source: 'route',
},
spans: expect.arrayContaining([
expect.objectContaining({
description: 'loading-post-456',
}),
]),
});
});

test('sends a navigation transaction with a parameterized URL', async ({ page }) => {
const pageloadTxnPromise = waitForTransaction('tanstack-router', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
});

const navigationTxnPromise = waitForTransaction('tanstack-router', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
});

await page.goto(`/`);
await pageloadTxnPromise;

await page.waitForTimeout(5000);
await page.locator('#nav-link').click();

const navigationTxn = await navigationTxnPromise;

expect(navigationTxn).toMatchObject({
contexts: {
trace: {
data: {
'sentry.source': 'route',
'sentry.origin': 'auto.navigation.react.tanstack_router',
'sentry.op': 'navigation',
'url.path.params.postId': '2',
},
op: 'navigation',
origin: 'auto.navigation.react.tanstack_router',
},
},
transaction: '/posts/$postId',
transaction_info: {
source: 'route',
},
spans: expect.arrayContaining([
expect.objectContaining({
description: 'loading-post-2',
}),
]),
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite';

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
define: {
__APP_DSN__: JSON.stringify(process.env.E2E_TEST_DSN),
},
preview: {
port: 3030,
},
});
Loading