Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"clean": "npx rimraf node_modules pnpm-lock.yaml",
"test:e2e": "playwright test",
"test:build": "pnpm install && pnpm build",
"test:assert": "pnpm test:e2e"
"test:assert": "pnpm test:e2e",
"test:build:tunnel": "pnpm install && E2E_TEST_TUNNEL=1 E2E_TEST_DSN=http://public@localhost:3031/1337 PUBLIC_E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm build",
"test:assert:tunnel": "E2E_TEST_TUNNEL=1 E2E_TEST_DSN=http://public@localhost:3031/1337 PUBLIC_E2E_TEST_DSN=http://public@localhost:3031/1337 playwright test tunnel"
},
"dependencies": {
"@sentry/sveltekit": "file:../../packed/sentry-sveltekit-packed.tgz"
Expand All @@ -30,5 +32,14 @@
},
"volta": {
"extends": "../../package.json"
},
"sentryTest": {
"variants": [
{
"label": "sveltekit-3-cloudflare-workers (tunnel)",
"build-command": "pnpm test:build:tunnel",
"assert-command": "pnpm test:assert:tunnel"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// `App.Platform` (including `platform.ctx`) is declared by `@sveltejs/adapter-cloudflare`.
declare global {
const __E2E_TEST_TUNNEL__: boolean;

namespace App {
// interface Error {}
// interface Locals {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as Sentry from '@sentry/sveltekit';
Sentry.init({
environment: 'qa',
dsn: PUBLIC_E2E_TEST_DSN,
tunnel: 'http://localhost:3031/', // proxy server
tunnel: __E2E_TEST_TUNNEL__ ? true : 'http://localhost:3031/', // proxy server
tracesSampleRate: 1.0,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const handle = sequence(
environment: 'qa', // dynamic sampling bias to keep spans
dsn: E2E_TEST_DSN,
debug: !!process.env.DEBUG,
tunnel: 'http://localhost:3031/', // proxy server
tunnel: __E2E_TEST_TUNNEL__ ? true : 'http://localhost:3031/', // proxy server
tracesSampleRate: 1.0,
}),
sentryHandle(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { expect, test } from '@playwright/test';
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';

test.skip(!process.env.E2E_TEST_TUNNEL, 'Only runs in the tunnel variant');

const TUNNEL_PATH = /^\/app\/[a-z0-9]{1,7}$/;

test('serves the managed tunnel from the Worker under the base path', async ({ page }) => {
const pageloadSpanPromise = waitForStreamedSpan(
'sveltekit-3-cloudflare-workers',
span => getSpanOp(span) === 'pageload' && span.is_segment,
);
const tunnelServerSpanPromise = waitForStreamedSpan(
'sveltekit-3-cloudflare-workers',
span => getSpanOp(span) === 'http.server' && TUNNEL_PATH.test(String(span.attributes['url.path']?.value ?? '')),
);
const tunnelResponsePromise = page.waitForResponse(
response => response.request().method() === 'POST' && TUNNEL_PATH.test(new URL(response.url()).pathname),
);

await page.goto('/app/');
const tunnelResponse = await tunnelResponsePromise;

expect(tunnelResponse.status()).toBe(200);
expect(new URL(tunnelResponse.url()).origin).toBe(new URL(page.url()).origin);
await expect(pageloadSpanPromise).resolves.toBeDefined();

// Anchor on a regular server span requested after the tunnel traffic: spans flush in order, so a tunnel span
// would arrive first.
const anchorSpanPromise = waitForStreamedSpan(
'sveltekit-3-cloudflare-workers',
span =>
getSpanOp(span) === 'http.server' &&
String(span.attributes['url.path']?.value ?? '').startsWith('/app/server-load-error'),
);
await page.request.get('/app/server-load-error');

const winner = await Promise.race([
tunnelServerSpanPromise.then(() => 'tunnel' as const),
anchorSpanPromise.then(() => 'anchor' as const),
]);
expect(winner).toBe('anchor');
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import { defineConfig } from 'vite';

// The tunnel variant serves the app under a base path to cover that the managed tunnel follows it.
const tunnelVariant = !!process.env.E2E_TEST_TUNNEL;

export default defineConfig({
define: {
__E2E_TEST_TUNNEL__: JSON.stringify(tunnelVariant),
},
plugins: [
sentrySvelteKit({
autoUploadSourceMaps: false,
Expand All @@ -17,6 +23,7 @@ export default defineConfig({
prerender: {
handleHttpError: 'ignore',
},
paths: tunnelVariant ? { base: '/app' } : undefined,
}),
],
});
21 changes: 19 additions & 2 deletions dev-packages/e2e-tests/test-applications/sveltekit-3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test:prod": "TEST_ENV=production playwright test",
"test:build": "pnpm install && pnpm build",
"test:assert": "pnpm test:prod"
"test:assert": "pnpm test:prod",
"test:build:tunnel": "pnpm install && E2E_TEST_TUNNEL=1 E2E_TEST_DSN=http://public@localhost:3031/1337 PUBLIC_E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm build",
"test:assert:tunnel": "E2E_TEST_TUNNEL=1 E2E_TEST_DSN=http://public@localhost:3031/1337 PUBLIC_E2E_TEST_DSN=http://public@localhost:3031/1337 TEST_ENV=production playwright test tunnel",
"test:assert:tunnel-dev": "E2E_TEST_TUNNEL=1 E2E_TEST_DSN=http://public@localhost:3031/1337 PUBLIC_E2E_TEST_DSN=http://public@localhost:3031/1337 TEST_ENV=development playwright test tunnel"
},
"dependencies": {
"@sentry/sveltekit": "file:../../packed/sentry-sveltekit-packed.tgz"
Expand All @@ -31,5 +34,19 @@
"volta": {
"extends": "../../package.json"
},
"type": "module"
"type": "module",
"sentryTest": {
"variants": [
{
"label": "sveltekit-3 (tunnel)",
"build-command": "pnpm test:build:tunnel",
"assert-command": "pnpm test:assert:tunnel"
},
{
"label": "sveltekit-3 (tunnel-dev)",
"build-command": "pnpm install",
"assert-command": "pnpm test:assert:tunnel-dev"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
const __E2E_TEST_TUNNEL__: boolean;

namespace App {
// interface Error {}
// interface Locals {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as Sentry from '@sentry/sveltekit';
Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: PUBLIC_E2E_TEST_DSN,
tunnel: `http://localhost:3031/`, // proxy server
tunnel: __E2E_TEST_TUNNEL__ ? true : `http://localhost:3031/`, // proxy server
tracesSampleRate: 1.0,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: E2E_TEST_DSN,
debug: !!process.env.DEBUG,
tunnel: `http://localhost:3031/`, // proxy server
tunnel: __E2E_TEST_TUNNEL__ ? true : `http://localhost:3031/`, // proxy server
tracesSampleRate: 1.0,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { expect, test } from '@playwright/test';
import { getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';

test.skip(!process.env.E2E_TEST_TUNNEL, 'Only runs in the tunnel variant');

const TUNNEL_PATH = /^\/app\/[a-z0-9]{1,7}$/;

test('sends client events through the managed tunnel under the base path', async ({ page }) => {
const errorEventPromise = waitForError(
'sveltekit-3',
event => event?.exception?.values?.[0]?.value === 'Click Error',
);
const tunnelServerSpanPromise = waitForStreamedSpan(
'sveltekit-3',
span => getSpanOp(span) === 'http.server' && TUNNEL_PATH.test(String(span.attributes['url.path']?.value ?? '')),
);

// The pageload envelope is the first tunnel request, and it's only sent once the app hydrated.
const tunnelResponsePromise = page.waitForResponse(
response => response.request().method() === 'POST' && TUNNEL_PATH.test(new URL(response.url()).pathname),
);
await page.goto('/app/client-error');
const tunnelResponse = await tunnelResponsePromise;

expect(tunnelResponse.status()).toBe(200);
expect(new URL(tunnelResponse.url()).origin).toBe(new URL(page.url()).origin);

await page.getByText('Throw error').click();
await expect(errorEventPromise).resolves.toBeDefined();

// Anchor on a regular server span requested after the tunnel traffic: spans flush in order, so a tunnel span
// would arrive first.
const anchorSpanPromise = waitForStreamedSpan(
'sveltekit-3',
span =>
getSpanOp(span) === 'http.server' && String(span.attributes['url.path']?.value ?? '').startsWith('/app/users'),
);
await page.request.get('/app/users/123');

const winner = await Promise.race([
tunnelServerSpanPromise.then(() => 'tunnel' as const),
anchorSpanPromise.then(() => 'anchor' as const),
]);
expect(winner).toBe('anchor');
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import { defineConfig } from 'vite';

// The tunnel variant serves the app under a base path to cover that the managed tunnel follows it.
const tunnelVariant = !!process.env.E2E_TEST_TUNNEL;

export default defineConfig({
define: {
__E2E_TEST_TUNNEL__: JSON.stringify(tunnelVariant),
},
plugins: [
sentrySvelteKit({
autoUploadSourceMaps: false,
Expand All @@ -22,6 +28,7 @@ export default defineConfig({
tracing: {
server: true,
},
paths: tunnelVariant ? { base: '/app' } : undefined,
}),
],
});
2 changes: 2 additions & 0 deletions packages/sveltekit/src/client/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Client, Integration } from '@sentry/core';
import { applySdkMetadata } from '@sentry/core';
import type { BrowserOptions } from '@sentry/svelte';
import { getDefaultIntegrations as getDefaultSvelteIntegrations, init as initSvelteSdk, WINDOW } from '@sentry/svelte';
import { applyTunnelBasePath } from '../common/tunnel';
import { browserTracingIntegration as svelteKitBrowserTracingIntegration } from './browserTracingIntegration';

type WindowWithSentryFetchProxy = typeof WINDOW & {
Expand All @@ -23,6 +24,7 @@ export function init(options: BrowserOptions): Client | undefined {
};

applySdkMetadata(opts, 'sveltekit', ['sveltekit', 'svelte']);
applyTunnelBasePath(opts);

// 1. Switch window.fetch to our fetch proxy we injected earlier
const actualFetch = switchToFetchProxy();
Expand Down
14 changes: 14 additions & 0 deletions packages/sveltekit/src/common/tunnel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Options } from '@sentry/core';
import { applyTunnelDefaults, GLOBAL_OBJ } from '@sentry/core';

/** Set by `sentrySvelteKit()` to the app's `paths.base`. */
export const TUNNEL_BASE_PATH_GLOBAL = '__sentry_sveltekit_base_path';

/**
* Serves the managed tunnel under the app's base path, which SvelteKit requires for every request it handles.
* A `basePath` the user set wins.
*/
export function applyTunnelBasePath(options: Pick<Options, 'tunnel'>): void {
const basePath = (GLOBAL_OBJ as { [TUNNEL_BASE_PATH_GLOBAL]?: string })[TUNNEL_BASE_PATH_GLOBAL];
options.tunnel = applyTunnelDefaults(options.tunnel, basePath ? { basePath } : undefined);
}
6 changes: 6 additions & 0 deletions packages/sveltekit/src/server-common/handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getDefaultIsolationScope,
getIsolationScope,
getTraceMetaTags,
handleTunnelRequestIfMatched,
hasSpanStreamingEnabled,
httpHeadersToSpanAttributes,
HTTP_SPAN_NAME_FALLBACK,
Expand Down Expand Up @@ -291,6 +292,11 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle {
};

const sentryRequestHandler: Handle = input => {
const tunnelResponse = handleTunnelRequestIfMatched(input.event.request);
if (tunnelResponse) {
return tunnelResponse;
}

const backwardsForwardsCompatibleEvent = input.event as typeof input.event & BackwardsForwardsCompatibleEvent;

// Escape hatch to suppress request isolation and trace continuation (see initCloudflareSentryHandle)
Expand Down
2 changes: 2 additions & 0 deletions packages/sveltekit/src/server/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { applySdkMetadata } from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { getDefaultIntegrations as getDefaultNodeIntegrations, init as initNodeSdk } from '@sentry/node';
import { applyTunnelBasePath } from '../common/tunnel';
import { rewriteFramesIntegration } from '../server-common/integrations/rewriteFramesIntegration';
import { svelteKitSpansIntegration } from '../server-common/integrations/svelteKitSpans';
import { httpIntegration } from './integrations/http';
Expand All @@ -26,6 +27,7 @@ export function init(options: NodeOptions): NodeClient | undefined {
};

applySdkMetadata(opts, 'sveltekit', ['sveltekit', 'node']);
applyTunnelBasePath(opts);

return initNodeSdk(opts);
}
2 changes: 2 additions & 0 deletions packages/sveltekit/src/vite/sentryVitePlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { makeGlobalValuesInjectionPlugin } from './injectGlobalValues';
import { createKitConfigResolver } from './kitConfig';
import { makeCustomSentryVitePlugins } from './sourceMaps';
import { getAdapterOutputDir } from './svelteConfig';
import { makeTunnelBasePathPlugin } from './tunnelBasePath';
import type { CustomSentryVitePluginOptions, SentrySvelteKitPluginOptions } from './types';

const DEFAULT_PLUGIN_OPTIONS: SentrySvelteKitPluginOptions = {
Expand Down Expand Up @@ -56,6 +57,7 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}
kitConfigResolver.plugin,
makeBrowserTracingVariantResolverPlugin(),
makeOpenTelemetryApiResolverPlugin(),
makeTunnelBasePathPlugin(getKitConfig),
];

if (mergedOptions.autoInstrument) {
Expand Down
50 changes: 50 additions & 0 deletions packages/sveltekit/src/vite/tunnelBasePath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { escapeStringForRegex } from '@sentry/core';
import MagicString from 'magic-string';
import type { Plugin } from 'vite';
import { TUNNEL_BASE_PATH_GLOBAL } from '../common/tunnel';
import type { ResolvedKitConfig } from './kitConfig';
import { getHooksFileName } from './svelteConfig';

const VIRTUAL_BASE_PATH_FILE = '\0sentry-sveltekit-tunnel-base-path';

/**
* Hands the app's `paths.base` to the client and server SDKs so `tunnel: true` works under a base path without
* the user repeating it. It's imported first in the hooks files, so it's set before any `init` they import.
*/
export function makeTunnelBasePathPlugin(getKitConfig: () => Promise<ResolvedKitConfig>): Plugin {
let basePath = '';
let entryFileRegexp: RegExp | undefined;

return {
name: 'sentry-sveltekit-tunnel-base-path',

configResolved: async () => {
const kitConfig = await getKitConfig();
basePath = kitConfig.paths?.base || '';

const hooksFiles = [getHooksFileName(kitConfig, 'server'), getHooksFileName(kitConfig, 'client')]
.map(escapeStringForRegex)
.join('|');
// oxlint-disable-next-line sdk/no-regexp-constructor -- not end user input + escaped anyway
entryFileRegexp = new RegExp(`/(?:${hooksFiles}|src/instrumentation\\.server)\\.(?:js|ts|mjs|mts)$`);
},

resolveId: id => (id === VIRTUAL_BASE_PATH_FILE ? id : null),

load: id =>
id === VIRTUAL_BASE_PATH_FILE
? `globalThis[${JSON.stringify(TUNNEL_BASE_PATH_GLOBAL)}] = ${JSON.stringify(basePath)};`
: null,

transform: (code, id) => {
if (!basePath || !entryFileRegexp?.test(id)) {
return null;
}

const ms = new MagicString(code);
ms.prepend(`import ${JSON.stringify(VIRTUAL_BASE_PATH_FILE)};\n`);

return { code: ms.toString(), map: ms.generateMap({ hires: true }) };
},
};
}
2 changes: 2 additions & 0 deletions packages/sveltekit/src/worker/cloudflare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@sentry/cloudflare';
import { addNonEnumerableProperty } from '@sentry/core';
import type { Handle } from '@sveltejs/kit';
import { applyTunnelBasePath } from '../common/tunnel';
import { rewriteFramesIntegration } from '../server-common/integrations/rewriteFramesIntegration';
import { svelteKitSpansIntegration } from '../server-common/integrations/svelteKitSpans';
import { getCloudflareExecutionContext } from '../server-common/utils';
Expand All @@ -31,6 +32,7 @@ export function initCloudflareSentryHandle(options: CloudflareOptions): Handle {
...options,
};

applyTunnelBasePath(opts);
setAsyncLocalStorageAsyncContextStrategy();

const handleInitSentry: Handle = ({ event, resolve }) => {
Expand Down
Loading
Loading