From 21cf1bcdffda598d58913eeafd8e9ead5a1dd4d7 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Wed, 2 Sep 2026 15:00:16 +0200 Subject: [PATCH] test(e2e): Port Solid E2E test apps to span streaming Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Xo7LcmA5Cte2uiSmp7hXSN --- .../test-applications/solid-static/.gitignore | 29 ++ .../test-applications/solid-static/README.md | 40 +++ .../test-applications/solid-static/index.html | 15 + .../solid-static/package.json | 33 ++ .../solid-static/playwright.config.mjs | 8 + .../solid-static/postcss.config.js | 6 + .../solid-static/src/app.tsx | 72 ++++ .../solid-static/src/index.css | 3 + .../solid-static/src/index.tsx | 18 + .../solid-static/src/pageroot.tsx | 28 ++ .../solid-static/src/routes.ts | 23 ++ .../solid-static/start-event-proxy.mjs | 6 + .../solid-static/tailwind.config.ts | 11 + .../solid-static/tests/errorboundary.test.ts | 93 ++++++ .../solid-static/tests/errors.test.ts | 28 ++ .../solid-static/tests/performance.test.ts | 23 ++ .../solid-static/tsconfig.json | 14 + .../solid-static/vite.config.ts | 10 + .../solid-tanstack-router/src/main.tsx | 1 - .../tests/routing-instrumentation.test.ts | 307 +++++++----------- .../test-applications/solid/src/index.tsx | 1 - .../solid/tests/performance.test.ts | 26 +- 22 files changed, 590 insertions(+), 205 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/README.md create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/index.html create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/package.json create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/postcss.config.js create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/src/app.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/src/index.css create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/src/index.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/src/pageroot.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/src/routes.ts create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/tailwind.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/tests/errorboundary.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/tests/performance.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/solid-static/vite.config.ts diff --git a/dev-packages/e2e-tests/test-applications/solid-static/.gitignore b/dev-packages/e2e-tests/test-applications/solid-static/.gitignore new file mode 100644 index 000000000000..84634c973eeb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/.gitignore @@ -0,0 +1,29 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +/test-results/ +/playwright-report/ +/playwright/.cache/ + +!*.d.ts diff --git a/dev-packages/e2e-tests/test-applications/solid-static/README.md b/dev-packages/e2e-tests/test-applications/solid-static/README.md new file mode 100644 index 000000000000..81e5eb6c2d40 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/README.md @@ -0,0 +1,40 @@ +## Usage + +Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`. + +This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely +be removed once you clone a template. + +```bash +$ npm install # or pnpm install or yarn install +``` + +## Exploring the template + +This template's goal is to showcase the routing features of Solid. It also showcase how the router and Suspense work +together to parallelize data fetching tied to a route via the `.data.ts` pattern. + +You can learn more about it on the [`@solidjs/router` repository](https://github.com/solidjs/solid-router) + +### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs) + +## Available Scripts + +In the project directory, you can run: + +### `npm run dev` or `npm start` + +Runs the app in the development mode.
Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.
+ +### `npm run build` + +Builds the app for production to the `dist` folder.
It correctly bundles Solid in production mode and optimizes the +build for the best performance. + +The build is minified and the filenames include the hashes.
Your app is ready to be deployed! + +## Deployment + +You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.) diff --git a/dev-packages/e2e-tests/test-applications/solid-static/index.html b/dev-packages/e2e-tests/test-applications/solid-static/index.html new file mode 100644 index 000000000000..5ede1234b60e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/index.html @@ -0,0 +1,15 @@ + + + + + + + Solid App + + + +
+ + + + diff --git a/dev-packages/e2e-tests/test-applications/solid-static/package.json b/dev-packages/e2e-tests/test-applications/solid-static/package.json new file mode 100644 index 000000000000..a0f775f30727 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/package.json @@ -0,0 +1,33 @@ +{ + "name": "solid-static", + "version": "0.0.0", + "description": "", + "scripts": { + "build": "vite build", + "clean": "npx rimraf node_modules pnpm-lock.yaml dist", + "dev": "vite", + "preview": "vite preview", + "start": "vite", + "test:prod": "TEST_ENV=production playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test:prod" + }, + "license": "MIT", + "devDependencies": { + "@playwright/test": "~1.56.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "autoprefixer": "^10.4.17", + "postcss": "^8.4.33", + "solid-devtools": "^0.29.2", + "tailwindcss": "^3.4.1", + "vite": "^5.4.11", + "vite-plugin-solid": "^2.11.6" + }, + "dependencies": { + "solid-js": "^1.8.18", + "@sentry/solid": "file:../../packed/sentry-solid-packed.tgz" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/solid-static/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/solid-static/playwright.config.mjs new file mode 100644 index 000000000000..0c468af7d879 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/playwright.config.mjs @@ -0,0 +1,8 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: 'pnpm preview --port 3030', + port: 3030, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/solid-static/postcss.config.js b/dev-packages/e2e-tests/test-applications/solid-static/postcss.config.js new file mode 100644 index 000000000000..12a703d900da --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/dev-packages/e2e-tests/test-applications/solid-static/src/app.tsx b/dev-packages/e2e-tests/test-applications/solid-static/src/app.tsx new file mode 100644 index 000000000000..0b83d62ff655 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/src/app.tsx @@ -0,0 +1,72 @@ +import * as Sentry from '@sentry/solid'; +import { ErrorBoundary, createSignal, onMount } from 'solid-js'; + +const SentryErrorBoundary = Sentry.withSentryErrorBoundary(ErrorBoundary); + +const [count, setCount] = createSignal(1); +const [caughtError, setCaughtError] = createSignal(false); + +export default function App() { + return ( + + {caughtError() && } +
+
+ +
+
+ +
+
+
+ ); +} + +function Throw(props) { + onMount(() => { + throw new Error(props.error); + }); + return null; +} + +function SampleErrorBoundary(props) { + return ( + ( +
+

Error Boundary Fallback

+
+ {error.message} +
+ +
+ )} + > + {props.children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/solid-static/src/index.css b/dev-packages/e2e-tests/test-applications/solid-static/src/index.css new file mode 100644 index 000000000000..b5c61c956711 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/dev-packages/e2e-tests/test-applications/solid-static/src/index.tsx b/dev-packages/e2e-tests/test-applications/solid-static/src/index.tsx new file mode 100644 index 000000000000..383330dafc2f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/src/index.tsx @@ -0,0 +1,18 @@ +/* @refresh reload */ +import * as Sentry from '@sentry/solid'; +import { render } from 'solid-js/web'; +import App from './app'; +import './index.css'; + +Sentry.init({ + traceLifecycle: 'static', + dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, + debug: true, + environment: 'qa', // dynamic sampling bias to keep transactions + integrations: [Sentry.browserTracingIntegration()], + release: 'e2e-test', + tunnel: 'http://localhost:3031/', // proxy server + tracesSampleRate: 1.0, +}); + +render(() => , document.getElementById('root')); diff --git a/dev-packages/e2e-tests/test-applications/solid-static/src/pageroot.tsx b/dev-packages/e2e-tests/test-applications/solid-static/src/pageroot.tsx new file mode 100644 index 000000000000..0919c0e362db --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/src/pageroot.tsx @@ -0,0 +1,28 @@ +import { A } from '@solidjs/router'; + +export default function PageRoot(props) { + return ( + <> + +
{props.children}
+ + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solid-static/src/routes.ts b/dev-packages/e2e-tests/test-applications/solid-static/src/routes.ts new file mode 100644 index 000000000000..96b78e113ef5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/src/routes.ts @@ -0,0 +1,23 @@ +import { lazy } from 'solid-js'; + +import ErrorBoundaryExample from './pages/errorboundaryexample'; +import Home from './pages/home'; + +export const routes = [ + { + path: '/', + component: Home, + }, + { + path: '/user/:id', + component: lazy(() => import('./pages/user')), + }, + { + path: '/error-boundary-example', + component: ErrorBoundaryExample, + }, + { + path: '**', + component: lazy(() => import('./errors/404')), + }, +]; diff --git a/dev-packages/e2e-tests/test-applications/solid-static/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/solid-static/start-event-proxy.mjs new file mode 100644 index 000000000000..2c25e2c1fe1c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'solid-static', +}); diff --git a/dev-packages/e2e-tests/test-applications/solid-static/tailwind.config.ts b/dev-packages/e2e-tests/test-applications/solid-static/tailwind.config.ts new file mode 100644 index 000000000000..f69a95185570 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/tailwind.config.ts @@ -0,0 +1,11 @@ +import type { Config } from 'tailwindcss'; + +const config: Config = { + content: ['./src/**/*.{js,jsx,ts,tsx}'], + theme: { + extend: {}, + }, + plugins: [], +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/solid-static/tests/errorboundary.test.ts b/dev-packages/e2e-tests/test-applications/solid-static/tests/errorboundary.test.ts new file mode 100644 index 000000000000..797b91b36780 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/tests/errorboundary.test.ts @@ -0,0 +1,93 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('captures an exception', async ({ page }) => { + const errorEventPromise = waitForError('solid-static', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === 'Error 1 thrown from Sentry ErrorBoundary in Solid E2E test app' + ); + }); + + const [, , errorEvent] = await Promise.all([ + page.goto('/'), + page.locator('#caughtErrorBtn').click(), + errorEventPromise, + ]); + + expect(errorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 1 thrown from Sentry ErrorBoundary in Solid E2E test app', + mechanism: { + type: 'auto.function.solid.error_boundary', + handled: true, + }, + }, + ], + }, + transaction: '/', + }); +}); + +test('captures a second exception after resetting the boundary', async ({ page }) => { + const firstErrorEventPromise = waitForError('solid-static', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === 'Error 1 thrown from Sentry ErrorBoundary in Solid E2E test app' + ); + }); + + const [, , firstErrorEvent] = await Promise.all([ + page.goto('/'), + page.locator('#caughtErrorBtn').click(), + firstErrorEventPromise, + ]); + + expect(firstErrorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 1 thrown from Sentry ErrorBoundary in Solid E2E test app', + mechanism: { + type: 'auto.function.solid.error_boundary', + handled: true, + }, + }, + ], + }, + transaction: '/', + }); + + const secondErrorEventPromise = waitForError('solid-static', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === 'Error 2 thrown from Sentry ErrorBoundary in Solid E2E test app' + ); + }); + + const [, , secondErrorEvent] = await Promise.all([ + page.locator('#errorBoundaryResetBtn').click(), + page.locator('#caughtErrorBtn').click(), + await secondErrorEventPromise, + ]); + + expect(secondErrorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 2 thrown from Sentry ErrorBoundary in Solid E2E test app', + mechanism: { + type: 'auto.function.solid.error_boundary', + handled: true, + }, + }, + ], + }, + transaction: '/', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solid-static/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/solid-static/tests/errors.test.ts new file mode 100644 index 000000000000..eff1e4ed3417 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/tests/errors.test.ts @@ -0,0 +1,28 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('sends an error', async ({ page }) => { + const errorPromise = waitForError('solid-static', async errorEvent => { + return !errorEvent.type && errorEvent.exception?.values?.[0]?.value === 'Error thrown from Solid E2E test app'; + }); + + await Promise.all([page.goto(`/`), page.locator('#errorBtn').click()]); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Solid E2E test app', + mechanism: { + type: 'auto.browser.global_handlers.onerror', + handled: false, + }, + }, + ], + }, + transaction: '/', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solid-static/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/solid-static/tests/performance.test.ts new file mode 100644 index 000000000000..dae29fc8e377 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/tests/performance.test.ts @@ -0,0 +1,23 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends a pageload transaction', async ({ page }) => { + const transactionPromise = waitForTransaction('solid-static', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const [, pageloadTransaction] = await Promise.all([page.goto('/'), transactionPromise]); + + expect(pageloadTransaction).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.browser', + }, + }, + transaction: '/', + transaction_info: { + source: 'url', + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solid-static/tsconfig.json b/dev-packages/e2e-tests/test-applications/solid-static/tsconfig.json new file mode 100644 index 000000000000..5d2faf0af117 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["vite/client"], + "noEmit": true, + "isolatedModules": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/solid-static/vite.config.ts b/dev-packages/e2e-tests/test-applications/solid-static/vite.config.ts new file mode 100644 index 000000000000..d1835ee1b8ff --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solid-static/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import solidPlugin from 'vite-plugin-solid'; + +export default defineConfig({ + plugins: [solidPlugin()], + build: { + target: 'esnext', + }, + envPrefix: 'PUBLIC_', +}); diff --git a/dev-packages/e2e-tests/test-applications/solid-tanstack-router/src/main.tsx b/dev-packages/e2e-tests/test-applications/solid-tanstack-router/src/main.tsx index 9caba73259d2..2c9df09d5b43 100644 --- a/dev-packages/e2e-tests/test-applications/solid-tanstack-router/src/main.tsx +++ b/dev-packages/e2e-tests/test-applications/solid-tanstack-router/src/main.tsx @@ -97,7 +97,6 @@ declare module '@tanstack/solid-router' { declare const __APP_DSN__: string; Sentry.init({ - traceLifecycle: 'static', dsn: __APP_DSN__, debug: true, environment: 'qa', // dynamic sampling bias to keep transactions diff --git a/dev-packages/e2e-tests/test-applications/solid-tanstack-router/tests/routing-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/solid-tanstack-router/tests/routing-instrumentation.test.ts index 83690b4a4e5f..db937c1602c2 100644 --- a/dev-packages/e2e-tests/test-applications/solid-tanstack-router/tests/routing-instrumentation.test.ts +++ b/dev-packages/e2e-tests/test-applications/solid-tanstack-router/tests/routing-instrumentation.test.ts @@ -1,250 +1,183 @@ 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 with a parameterized URL', async ({ page }) => { - const transactionPromise = waitForTransaction('solid-tanstack-router', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a pageload span with a parameterized URL', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('solid-tanstack-router', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/posts/456`); - const rootSpan = await transactionPromise; - - expect(rootSpan).toMatchObject({ - contexts: { - trace: { - data: { - 'sentry.segment.name.source': 'route', - 'sentry.origin': 'auto.pageload.solid.tanstack_router', - 'sentry.op': 'pageload', - 'url.path.parameter.postId': '456', - 'url.template': '/posts/$postId', - 'url.path': '/posts/456', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/456$/), - }, - op: 'pageload', - origin: 'auto.pageload.solid.tanstack_router', - }, - }, - transaction: '/posts/$postId', - transaction_info: { - source: 'route', - }, + const pageloadSpan = await pageloadSpanPromise; + + expect(pageloadSpan.name).toBe('/posts/$postId'); + expect(pageloadSpan.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.solid.tanstack_router', type: 'string' }, + 'sentry.op': { value: 'pageload', type: 'string' }, + 'url.path.parameter.postId': { value: '456', type: 'string' }, + 'url.template': { value: '/posts/$postId', type: 'string' }, + 'url.path': { value: '/posts/456', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/456$/), type: 'string' }, }); }); -test('sends a navigation transaction with a parameterized URL', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('solid-tanstack-router', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a navigation span with a parameterized URL', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('solid-tanstack-router', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); - const navigationTxnPromise = waitForTransaction('solid-tanstack-router', async transactionEvent => { - return ( - !!transactionEvent?.transaction && - transactionEvent.transaction === '/posts/$postId' && - transactionEvent.contexts?.trace?.op === 'navigation' - ); + const navigationSpanPromise = waitForStreamedSpan('solid-tanstack-router', span => { + return getSpanOp(span) === 'navigation' && span.is_segment && span.name === '/posts/$postId'; }); await page.goto(`/`); - await pageloadTxnPromise; + await pageloadSpanPromise; await page.waitForTimeout(5000); await page.locator('#nav-link').click(); - const navigationTxn = await navigationTxnPromise; - - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - data: { - 'sentry.segment.name.source': 'route', - 'sentry.origin': 'auto.navigation.solid.tanstack_router', - 'sentry.op': 'navigation', - 'url.path.parameter.postId': '2', - 'url.template': '/posts/$postId', - 'url.path': '/posts/2', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/2$/), - }, - op: 'navigation', - origin: 'auto.navigation.solid.tanstack_router', - }, - }, - transaction: '/posts/$postId', - transaction_info: { - source: 'route', - }, + const navigationSpan = await navigationSpanPromise; + + expect(navigationSpan.name).toBe('/posts/$postId'); + expect(navigationSpan.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.solid.tanstack_router', type: 'string' }, + 'sentry.op': { value: 'navigation', type: 'string' }, + 'url.path.parameter.postId': { value: '2', type: 'string' }, + 'url.template': { value: '/posts/$postId', type: 'string' }, + 'url.path': { value: '/posts/2', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/2$/), type: 'string' }, }); }); -test('sends a pageload transaction named after the resolved route when a redirect is thrown on initial load', async ({ +test('sends a pageload span named after the resolved route when a redirect is thrown on initial load', async ({ page, }) => { - const pageloadTxnPromise = waitForTransaction('solid-tanstack-router', async transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/posts/$postId'; + const pageloadSpanPromise = waitForStreamedSpan('solid-tanstack-router', span => { + return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/posts/$postId'; }); await page.goto(`/redirect`); - const pageloadTxn = await pageloadTxnPromise; - - expect(pageloadTxn).toMatchObject({ - contexts: { - trace: { - data: { - 'sentry.segment.name.source': 'route', - 'sentry.origin': 'auto.pageload.solid.tanstack_router', - 'sentry.op': 'pageload', - 'url.path.parameter.postId': '1', - 'url.template': '/posts/$postId', - 'url.path': '/posts/1', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/1$/), - }, - op: 'pageload', - origin: 'auto.pageload.solid.tanstack_router', - }, - }, - transaction: '/posts/$postId', - transaction_info: { - source: 'route', - }, + const pageloadSpan = await pageloadSpanPromise; + + expect(pageloadSpan.name).toBe('/posts/$postId'); + expect(pageloadSpan.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.solid.tanstack_router', type: 'string' }, + 'sentry.op': { value: 'pageload', type: 'string' }, + 'url.path.parameter.postId': { value: '1', type: 'string' }, + 'url.template': { value: '/posts/$postId', type: 'string' }, + 'url.path': { value: '/posts/1', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/1$/), type: 'string' }, }); }); -test('sends a navigation transaction when a redirect is thrown in beforeLoad', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('solid-tanstack-router', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a navigation span when a redirect is thrown in beforeLoad', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('solid-tanstack-router', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); - const navigationTxnPromise = waitForTransaction('solid-tanstack-router', async transactionEvent => { - return ( - !!transactionEvent?.transaction && - transactionEvent.transaction === '/posts/$postId' && - transactionEvent.contexts?.trace?.op === 'navigation' - ); + const navigationSpanPromise = waitForStreamedSpan('solid-tanstack-router', span => { + return getSpanOp(span) === 'navigation' && span.is_segment && span.name === '/posts/$postId'; }); await page.goto(`/`); - await pageloadTxnPromise; + await pageloadSpanPromise; await page.locator('#redirect-link').click(); - const navigationTxn = await navigationTxnPromise; - - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - data: { - 'sentry.segment.name.source': 'route', - 'sentry.origin': 'auto.navigation.solid.tanstack_router', - 'sentry.op': 'navigation', - 'url.path.parameter.postId': '1', - 'url.template': '/posts/$postId', - 'url.path': '/posts/1', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/1$/), - }, - op: 'navigation', - origin: 'auto.navigation.solid.tanstack_router', - }, - }, - transaction: '/posts/$postId', - transaction_info: { - source: 'route', - }, + const navigationSpan = await navigationSpanPromise; + + expect(navigationSpan.name).toBe('/posts/$postId'); + expect(navigationSpan.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.solid.tanstack_router', type: 'string' }, + 'sentry.op': { value: 'navigation', type: 'string' }, + 'url.path.parameter.postId': { value: '1', type: 'string' }, + 'url.template': { value: '/posts/$postId', type: 'string' }, + 'url.path': { value: '/posts/1', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/1$/), type: 'string' }, }); }); -test('sends a navigation transaction for a normal navigation that happens after a redirect', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('solid-tanstack-router', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a navigation span for a normal navigation that happens after a redirect', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('solid-tanstack-router', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/`); - await pageloadTxnPromise; + await pageloadSpanPromise; - const redirectTxnPromise = waitForTransaction('solid-tanstack-router', async transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === '/posts/$postId'; + const redirectSpanPromise = waitForStreamedSpan('solid-tanstack-router', span => { + return getSpanOp(span) === 'navigation' && span.is_segment && span.name === '/posts/$postId'; }); await page.locator('#redirect-link').click(); - await redirectTxnPromise; + await redirectSpanPromise; - const navigationTxnPromise = waitForTransaction('solid-tanstack-router', async transactionEvent => { + const navigationSpanPromise = waitForStreamedSpan('solid-tanstack-router', span => { return ( - transactionEvent.contexts?.trace?.op === 'navigation' && - transactionEvent.contexts?.trace?.data?.['url.path.parameter.postId'] === '2' + getSpanOp(span) === 'navigation' && span.is_segment && span.attributes['url.path.parameter.postId']?.value === '2' ); }); await page.locator('#nav-link').click(); - const navigationTxn = await navigationTxnPromise; - - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - data: { - 'sentry.segment.name.source': 'route', - 'sentry.origin': 'auto.navigation.solid.tanstack_router', - 'sentry.op': 'navigation', - 'url.path.parameter.postId': '2', - 'url.template': '/posts/$postId', - 'url.path': '/posts/2', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/2$/), - }, - op: 'navigation', - origin: 'auto.navigation.solid.tanstack_router', - }, - }, - transaction: '/posts/$postId', - transaction_info: { - source: 'route', - }, + const navigationSpan = await navigationSpanPromise; + + expect(navigationSpan.name).toBe('/posts/$postId'); + expect(navigationSpan.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.solid.tanstack_router', type: 'string' }, + 'sentry.op': { value: 'navigation', type: 'string' }, + 'url.path.parameter.postId': { value: '2', type: 'string' }, + 'url.template': { value: '/posts/$postId', type: 'string' }, + 'url.path': { value: '/posts/2', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/2$/), type: 'string' }, }); }); -test('sends pageload transaction with web vitals measurements', async ({ page }) => { - const transactionPromise = waitForTransaction('solid-tanstack-router', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a pageload span with web vital attributes and a standalone LCP span', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('solid-tanstack-router', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; + }); + + const lcpSpanPromise = waitForStreamedSpan('solid-tanstack-router', span => { + return getSpanOp(span) === 'ui.webvital.lcp'; }); await page.goto(`/`); - const transaction = await transactionPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.solid.tanstack_router', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/', - 'url.path': '/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), - }, - }, - }, - transaction: '/', - transaction_info: { - source: 'route', - }, - measurements: expect.objectContaining({ - ttfb: expect.objectContaining({ - value: expect.any(Number), - unit: 'millisecond', - }), - lcp: expect.objectContaining({ - value: expect.any(Number), - unit: 'millisecond', - }), - fp: expect.objectContaining({ - value: expect.any(Number), - unit: 'millisecond', - }), - fcp: expect.objectContaining({ - value: expect.any(Number), - unit: 'millisecond', - }), - }), + const pageloadSpan = await pageloadSpanPromise; + + // LCP is only reported once the page is hidden or a navigation happens + await page.evaluate(() => { + Object.defineProperty(document, 'visibilityState', { value: 'hidden', configurable: true }); + document.dispatchEvent(new Event('visibilitychange')); + }); + + const lcpSpan = await lcpSpanPromise; + + const webVitalNumber = { value: expect.any(Number), type: expect.stringMatching(/^(integer|double)$/) }; + + expect(pageloadSpan.name).toBe('/'); + expect(pageloadSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.solid.tanstack_router', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), type: 'string' }, + 'browser.web_vital.ttfb.value': webVitalNumber, + 'browser.web_vital.fp.value': webVitalNumber, + 'browser.web_vital.fcp.value': webVitalNumber, + }); + + expect(lcpSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'ui.webvital.lcp', type: 'string' }, + 'sentry.origin': { value: 'auto.http.browser.lcp', type: 'string' }, + 'sentry.pageload.span_id': { value: pageloadSpan.span_id, type: 'string' }, + 'browser.web_vital.lcp.value': webVitalNumber, }); }); diff --git a/dev-packages/e2e-tests/test-applications/solid/src/index.tsx b/dev-packages/e2e-tests/test-applications/solid/src/index.tsx index 383330dafc2f..882bb32d853a 100644 --- a/dev-packages/e2e-tests/test-applications/solid/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/solid/src/index.tsx @@ -5,7 +5,6 @@ import App from './app'; import './index.css'; Sentry.init({ - traceLifecycle: 'static', dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, debug: true, environment: 'qa', // dynamic sampling bias to keep transactions diff --git a/dev-packages/e2e-tests/test-applications/solid/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/solid/tests/performance.test.ts index a9bfbea82fb2..376fde71dbd7 100644 --- a/dev-packages/e2e-tests/test-applications/solid/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/solid/tests/performance.test.ts @@ -1,23 +1,17 @@ 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', async ({ page }) => { - const transactionPromise = waitForTransaction('solid', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a pageload span', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('solid', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); - const [, pageloadTransaction] = await Promise.all([page.goto('/'), transactionPromise]); + const [, pageloadSpan] = await Promise.all([page.goto('/'), pageloadSpanPromise]); - expect(pageloadTransaction).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.browser', - }, - }, - transaction: '/', - transaction_info: { - source: 'url', - }, + expect(pageloadSpan.name).toBe('Pageload'); + expect(pageloadSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.browser', type: 'string' }, + 'sentry.segment.name.source': { value: 'url', type: 'string' }, }); });