Repository navigation
feat(solidstart): Serve the managed tunnel on SolidStart v1 #25423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d0ebf84
4a40ad4
ba68ad9
ac5fb74
c375127
b4d0f5a
88a5051
bcf5afe
822096c
fd1363d
150131c
afaae1e
6c1a505
65cc48b
3ab6f3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
| import { isServerSpanAt, watchForTunnelSpan } from '@sentry-internal/test-utils'; | ||
|
|
||
| test.skip(!process.env.E2E_TEST_TUNNEL, 'Only runs in the tunnel variant'); | ||
|
|
||
| const APP_NAME = 'solidstart'; | ||
| const BASE_PATH = process.env.E2E_TEST_BASE_PATH ?? ''; | ||
| const TUNNEL_PATH = new RegExp(`^${BASE_PATH}/[a-z0-9]{1,7}$`); | ||
|
|
||
| test('answers the managed tunnel before the app middleware', async ({ page }) => { | ||
| // The variant's auth middleware rejects every other POST. | ||
| expect((await page.request.post(`${BASE_PATH}/`, { data: 'x' })).status()).toBe(401); | ||
|
|
||
| const raceTunnelSpan = watchForTunnelSpan(APP_NAME, isServerSpanAt(TUNNEL_PATH)); | ||
| const tunnelResponsePromise = page.waitForResponse( | ||
| response => response.request().method() === 'POST' && TUNNEL_PATH.test(new URL(response.url()).pathname), | ||
| ); | ||
|
|
||
| await page.goto(`${BASE_PATH}/`); | ||
|
|
||
| expect((await tunnelResponsePromise).status()).toBe(200); | ||
| expect( | ||
| await raceTunnelSpan(isServerSpanAt(`${BASE_PATH}/users/6`), () => page.request.get(`${BASE_PATH}/users/6`)), | ||
| ).toBe('anchor'); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import type { Options } from '@sentry/core'; | ||
| import { _INTERNAL_applyTunnelDefaults as applyTunnelDefaults } from '@sentry/core'; | ||
|
|
||
| /** Set on the server by `withSentry` to the app's `server.baseURL`. */ | ||
| export const TUNNEL_BASE_PATH_GLOBAL = '__sentry_solidstart_base_path'; | ||
|
|
||
| declare const __SENTRY_SOLIDSTART_TUNNEL_BASE_PATH__: string | undefined; | ||
|
|
||
| /** | ||
| * Serves the browser's managed tunnel under the app's `server.baseURL`, which `withSentry` defines at build time. | ||
| * A `basePath` the user set wins. | ||
| */ | ||
| export function applyTunnelBasePath(options: Pick<Options, 'tunnel'>): void { | ||
| const basePath = | ||
| typeof __SENTRY_SOLIDSTART_TUNNEL_BASE_PATH__ !== 'undefined' ? __SENTRY_SOLIDSTART_TUNNEL_BASE_PATH__ : ''; | ||
| options.tunnel = applyTunnelDefaults(options.tunnel, { basePath }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import { GLOBAL_OBJ } from '@sentry/core'; | ||
| import { | ||
| handleTunnelRequestIfMatched, | ||
| isTunnelRequest, | ||
| _INTERNAL_setManagedTunnelDefaults as setManagedTunnelDefaults, | ||
| } from '@sentry/core/server'; | ||
| import { eventHandler, toWebRequest } from 'h3'; | ||
| import type { NitroAppPlugin } from 'nitropack'; | ||
| import { TUNNEL_BASE_PATH_GLOBAL } from '../common/tunnel'; | ||
|
|
||
| /** | ||
| * Answers managed tunnel requests on SolidStart v1 (Nitro v2), from the first layer of the h3 app. That runs before | ||
| * route rules, the app's middleware and routing. `withSentry` registers it. | ||
| */ | ||
| export default (nitroApp => { | ||
| // The first layer sees the path before Nitro strips `server.baseURL`, so the tunnel is served under it. The server | ||
| // bundle's banner sets the global before this plugin runs. | ||
| setManagedTunnelDefaults({ | ||
| basePath: (GLOBAL_OBJ as { [TUNNEL_BASE_PATH_GLOBAL]?: string })[TUNNEL_BASE_PATH_GLOBAL], | ||
| }); | ||
|
|
||
| // Not the `request` hook: h3 still runs the first layer after a hook answered, so route rules would act on the | ||
| // sent response. | ||
| nitroApp.h3App.stack.unshift({ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can create a helper for the nitro-specific hooks? They can be reused in nuxt, solidstart and tanstack start.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's see if we can do it later, I don't want to juggle multiple nitro versions right now, also tanstack got rid of |
||
| route: '', | ||
| handler: eventHandler(event => { | ||
| // Checked on the method and path first, so no other request's body gets wrapped in a web `Request`. | ||
| if (!isTunnelRequest({ method: event.method, url: event.path })) { | ||
| return undefined; | ||
| } | ||
|
|
||
| // `toWebRequest` also reads the body of the mock requests serverless presets pass through `localFetch`, which | ||
| // aren't Node streams. A returned `Response` is sent and ends the request, `undefined` hands it on. | ||
| return handleTunnelRequestIfMatched(toWebRequest(event)); | ||
| }), | ||
| }); | ||
| }) satisfies NitroAppPlugin; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { afterEach, describe, expect, it, vi } from 'vitest'; | ||
| import { applyTunnelBasePath } from '../../src/common/tunnel'; | ||
|
|
||
| describe('applyTunnelBasePath', () => { | ||
| afterEach(() => { | ||
| vi.unstubAllGlobals(); | ||
| }); | ||
|
|
||
| it('serves the managed tunnel under the defined base path', () => { | ||
| vi.stubGlobal('__SENTRY_SOLIDSTART_TUNNEL_BASE_PATH__', '/app'); | ||
| const options: Parameters<typeof applyTunnelBasePath>[0] = { tunnel: true }; | ||
|
|
||
| applyTunnelBasePath(options); | ||
|
|
||
| expect(options.tunnel).toEqual({ basePath: '/app' }); | ||
| }); | ||
|
|
||
| it('leaves the tunnel alone without a base path', () => { | ||
| const options: Parameters<typeof applyTunnelBasePath>[0] = { tunnel: true }; | ||
|
|
||
| applyTunnelBasePath(options); | ||
|
|
||
| expect(options.tunnel).toBe(true); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.