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
fddb9ef
45c695c
558782d
55e6bc8
72013e4
110dc70
e3ce2ee
a2b6e23
1976232
196de9e
d259c57
edc6c1c
009234b
58c5219
16fe235
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.