Skip to content

Commit

Permalink
feat(react): Add TanStack Router integration (#12095)
Browse files Browse the repository at this point in the history
Co-authored-by: Luca Forstner <luca.forstner@sentry.io>
  • Loading branch information
MicahLyle and lforst committed May 29, 2024
1 parent bbe7be5 commit 0d1093d
Show file tree
Hide file tree
Showing 16 changed files with 1,458 additions and 0 deletions.
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

0 comments on commit 0d1093d

Please sign in to comment.