diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 624e4d975b27..5ed034e8cdc2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -915,6 +915,7 @@ jobs: 'sveltekit', 'sveltekit-2', 'sveltekit-2-svelte-5', + 'sveltekit-2-twp', 'tanstack-router', 'generic-ts3.8', 'node-fastify', diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/test.ts index 76a618f79989..8fceec718447 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/tracing-without-performance/test.ts @@ -12,6 +12,38 @@ const META_TAG_PARENT_SPAN_ID = '1234567890123456'; const META_TAG_BAGGAGE = 'sentry-trace_id=12345678901234567890123456789012,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=prod'; +sentryTest('error on initial page has traceId from meta tag', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const errorEventPromise = getFirstSentryEnvelopeRequest( + page, + undefined, + eventAndTraceHeaderRequestParser, + ); + + await page.locator('#errorBtn').click(); + const [errorEvent, errorTraceHeader] = await errorEventPromise; + + expect(errorEvent.type).toEqual(undefined); + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: META_TAG_TRACE_ID, + parent_span_id: META_TAG_PARENT_SPAN_ID, + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + }); + + expect(errorTraceHeader).toEqual({ + environment: 'prod', + public_key: 'public', + release: '1.0.0', + trace_id: META_TAG_TRACE_ID, + }); +}); + sentryTest('error has new traceId after navigation', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/.gitignore b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/.gitignore new file mode 100644 index 000000000000..6635cf554275 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/.npmrc b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/README.md b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/README.md new file mode 100644 index 000000000000..888ba1591502 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/README.md @@ -0,0 +1,5 @@ +# Tracing Without Performance E2E test app + +E2E test app for testing Tracing Without Performance in a (SvelteKit) meta framework scenario + +Add tests to this app that specifically test TwP in meta frameworks. diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json new file mode 100644 index 000000000000..a88decc74e7b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json @@ -0,0 +1,35 @@ +{ + "name": "sveltekit-2-svelte-5", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "proxy": "node start-event-proxy.mjs", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "test:prod": "TEST_ENV=production playwright test", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:assert": "pnpm test:prod" + }, + "dependencies": { + "@sentry/sveltekit": "latest || *" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry/types": "latest || *", + "@sentry/utils": "latest || *", + "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "svelte": "^5.0.0-next.115", + "svelte-check": "^3.6.0", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "vite": "^5.0.3" + }, + "type": "module" +} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/playwright.config.mjs new file mode 100644 index 000000000000..0c468af7d879 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/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/sveltekit-2-twp/src/app.d.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/app.d.ts new file mode 100644 index 000000000000..ede601ab93e2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/app.html b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/app.html new file mode 100644 index 000000000000..77a5ff52c923 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/hooks.client.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/hooks.client.ts new file mode 100644 index 000000000000..25cedeee2d07 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/hooks.client.ts @@ -0,0 +1,15 @@ +import { env } from '$env/dynamic/public'; +import * as Sentry from '@sentry/sveltekit'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: env.PUBLIC_E2E_TEST_DSN, + release: '1.0.0', + tunnel: `http://localhost:3031/`, // proxy server +}); + +const myErrorHandler = ({ error, event }: any) => { + console.error('An error occurred on the client side:', error, event); +}; + +export const handleError = Sentry.handleErrorWithSentry(myErrorHandler); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/hooks.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/hooks.server.ts new file mode 100644 index 000000000000..e60e51b25968 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/hooks.server.ts @@ -0,0 +1,13 @@ +import { E2E_TEST_DSN } from '$env/static/private'; +import * as Sentry from '@sentry/sveltekit'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server +}); + +// not logging anything to console to avoid noise in the test output +export const handleError = Sentry.handleErrorWithSentry(() => {}); + +export const handle = Sentry.sentryHandle(); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/routes/+layout.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/routes/+layout.svelte new file mode 100644 index 000000000000..93e1191a8e7b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/routes/+layout.svelte @@ -0,0 +1,4 @@ +

Sveltekit E2E Test app

+
+ +
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/routes/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/routes/+page.svelte new file mode 100644 index 000000000000..bbf21b25c01b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/routes/+page.svelte @@ -0,0 +1,8 @@ +

Welcome to SvelteKit 2 with Svelte 5!

+

Visit kit.svelte.dev to read the documentation

+ + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/routes/errors/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/routes/errors/+page.server.ts new file mode 100644 index 000000000000..a8405926f90b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/routes/errors/+page.server.ts @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/sveltekit'; + +export const load = async ({ url }) => { + if (!url.search) { + Sentry.captureException(new Error('No search query provided')); + return { + error: 'No search query provided', + }; + } + return { + message: 'hi', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/routes/errors/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/routes/errors/+page.svelte new file mode 100644 index 000000000000..f0dde3f9b6d6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/src/routes/errors/+page.svelte @@ -0,0 +1,16 @@ + + +

+ Data: + {data.message || 'nothing'} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/start-event-proxy.mjs new file mode 100644 index 000000000000..01e1095d6956 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'sveltekit-2-twp', +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/static/favicon.png b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/static/favicon.png new file mode 100644 index 000000000000..825b9e65af7c Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/static/favicon.png differ diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/svelte.config.js b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/svelte.config.js new file mode 100644 index 000000000000..8a8cae634785 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/svelte.config.js @@ -0,0 +1,18 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://kit.svelte.dev/docs/adapters for more information about adapters. + adapter: adapter(), + }, +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/tests/errors.test.ts new file mode 100644 index 000000000000..0e16a2588982 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/tests/errors.test.ts @@ -0,0 +1,75 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('errors on frontend and backend are connected by the same trace', async ({ page }) => { + const clientErrorPromise = waitForError('sveltekit-2-twp', evt => { + return evt.exception?.values?.[0].value === 'Client Error'; + }); + + const serverErrorPromise = waitForError('sveltekit-2-twp', evt => { + return evt.exception?.values?.[0].value === 'No search query provided'; + }); + + await page.goto('/errors'); + + const clientError = await clientErrorPromise; + const serverError = await serverErrorPromise; + + expect(clientError).toMatchObject({ + contexts: { + trace: { + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + environment: 'qa', + exception: { + values: [ + { + mechanism: { + handled: false, + type: 'onunhandledrejection', + }, + stacktrace: expect.any(Object), + type: 'Error', + value: 'Client Error', + }, + ], + }, + level: 'error', + platform: 'javascript', + release: '1.0.0', + timestamp: expect.any(Number), + transaction: '/errors', + }); + + expect(serverError).toMatchObject({ + contexts: { + trace: { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + environment: 'qa', + exception: { + values: [ + { + mechanism: { + handled: true, + type: 'generic', + }, + stacktrace: {}, + }, + ], + }, + platform: 'node', + timestamp: expect.any(Number), + transaction: 'GET /errors', + }); + + const clientTraceId = clientError.contexts?.trace?.trace_id; + const serverTraceId = serverError.contexts?.trace?.trace_id; + + expect(clientTraceId).toBe(serverTraceId); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/tests/tracing.test.ts new file mode 100644 index 000000000000..6ed20d16b5e9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/tests/tracing.test.ts @@ -0,0 +1,23 @@ +import { expect, test } from '@playwright/test'; + +test('Initially loaded page contains trace meta tags from backend trace', async ({ page }) => { + await page.goto('/'); + + const sentryTraceMetaTag = page.locator('meta[name="sentry-trace"]').first(); + const sentryTraceContent = await sentryTraceMetaTag.getAttribute('content'); + + const baggageMetaTag = page.locator('meta[name="baggage"]').first(); + const baggageContent = await baggageMetaTag.getAttribute('content'); + + // ensure that we do not pass a sampled -1 or -0 flag at the end: + expect(sentryTraceContent).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}$/); + + expect(baggageContent?.length).toBeGreaterThan(0); + + const traceId = sentryTraceContent!.split('-')[0]; + + expect(baggageContent).toContain('sentry-environment=qa'); + expect(baggageContent).toContain(`sentry-trace_id=${traceId}`); + // ensure baggage also doesn't contain a sampled flag + expect(baggageContent).not.toContain('sentry-sampled='); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/tsconfig.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/tsconfig.json new file mode 100644 index 000000000000..ba6aa4e6610a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + }, + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/vite.config.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/vite.config.ts new file mode 100644 index 000000000000..1a410bee7e11 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/vite.config.ts @@ -0,0 +1,12 @@ +import { sentrySvelteKit } from '@sentry/sveltekit'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sentrySvelteKit({ + autoUploadSourceMaps: false, + }), + sveltekit(), + ], +}); diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index 7f4b8d980ad8..cee8520400c1 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -5,19 +5,16 @@ import { getCurrentScope, getDefaultIsolationScope, getIsolationScope, - getRootSpan, + getTraceMetaTags, setHttpStatus, - spanToTraceHeader, withIsolationScope, } from '@sentry/core'; import { startSpan } from '@sentry/core'; import { continueTrace } from '@sentry/node'; import type { Span } from '@sentry/types'; -import { dynamicSamplingContextToSentryBaggageHeader, logger, winterCGRequestToRequestData } from '@sentry/utils'; +import { logger, winterCGRequestToRequestData } from '@sentry/utils'; import type { Handle, ResolveOptions } from '@sveltejs/kit'; -import { getDynamicSamplingContextFromSpan } from '@sentry/opentelemetry'; - import { DEBUG_BUILD } from '../common/debug-build'; import { flushIfServerless, getTracePropagationData, sendErrorToSentry } from './utils'; @@ -81,25 +78,14 @@ export function addSentryCodeToPage(options: SentryHandleOptions): NonNullable { - const activeSpan = getActiveSpan(); - const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined; - if (rootSpan) { - const traceparentData = spanToTraceHeader(rootSpan); - const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader( - getDynamicSamplingContextFromSpan(rootSpan), - ); - const contentMeta = ` - - - `; - const contentScript = shouldInjectScript ? `` : ''; - - const content = `${contentMeta}\n${contentScript}`; - - return html.replace('', content); - } + const metaTags = getTraceMetaTags(); + const headWithMetaTags = metaTags ? `\n${metaTags}` : ''; + + const headWithFetchScript = shouldInjectScript ? `\n` : ''; + + const modifiedHead = `${headWithMetaTags}${headWithFetchScript}`; - return html; + return html.replace('', modifiedHead); }; } diff --git a/packages/sveltekit/test/server/handle.test.ts b/packages/sveltekit/test/server/handle.test.ts index ad49e9a16ef8..7b5b86d541b2 100644 --- a/packages/sveltekit/test/server/handle.test.ts +++ b/packages/sveltekit/test/server/handle.test.ts @@ -429,10 +429,14 @@ describe('addSentryCodeToPage', () => { `; - it('does not add meta tags if no active transaction', () => { + it("Adds add meta tags and fetch proxy script if there's no active transaction", () => { const transformPageChunk = addSentryCodeToPage({}); const transformed = transformPageChunk({ html, done: true }); - expect(transformed).toEqual(html); + + expect(transformed).toContain(' { @@ -442,6 +446,7 @@ describe('addSentryCodeToPage', () => { expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); }); }); @@ -453,18 +458,17 @@ describe('addSentryCodeToPage', () => { expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); }); }); it('does not add the fetch proxy script if the `injectFetchProxyScript` option is false', () => { const transformPageChunk = addSentryCodeToPage({ injectFetchProxyScript: false }); - SentryNode.startSpan({ name: 'test' }, () => { - const transformed = transformPageChunk({ html, done: true }) as string; + const transformed = transformPageChunk({ html, done: true }) as string; - expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); - }); + expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); }); }); diff --git a/packages/sveltekit/test/utils.ts b/packages/sveltekit/test/utils.ts index 993a6bd8823d..540db2882373 100644 --- a/packages/sveltekit/test/utils.ts +++ b/packages/sveltekit/test/utils.ts @@ -4,6 +4,7 @@ import { resolvedSyncPromise } from '@sentry/utils'; export function getDefaultNodeClientOptions(options: Partial = {}): ClientOptions { return { + dsn: 'http://examplePublicKey@localhost/0', integrations: [], transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})), stackParser: () => [],