From ef7b5e9c0610d33425a78a2aed4e2d251d08bfe7 Mon Sep 17 00:00:00 2001 From: Lazar Nikolov Date: Mon, 13 Apr 2026 15:30:40 -0400 Subject: [PATCH 1/7] Add a TanStack Start tunnel route helper --- .../tanstackstart-react/src/server/index.ts | 1 + .../src/server/tunnelRoute.ts | 43 +++ .../test/server/tunnelRoute.test.ts | 48 +++ yarn.lock | 307 +----------------- 4 files changed, 100 insertions(+), 299 deletions(-) create mode 100644 packages/tanstackstart-react/src/server/tunnelRoute.ts create mode 100644 packages/tanstackstart-react/test/server/tunnelRoute.test.ts diff --git a/packages/tanstackstart-react/src/server/index.ts b/packages/tanstackstart-react/src/server/index.ts index 4fe781b6d778..0ae0968e574b 100644 --- a/packages/tanstackstart-react/src/server/index.ts +++ b/packages/tanstackstart-react/src/server/index.ts @@ -9,6 +9,7 @@ export { init } from './sdk'; export { wrapFetchWithSentry } from './wrapFetchWithSentry'; export { wrapMiddlewaresWithSentry } from './middleware'; export { sentryGlobalRequestMiddleware, sentryGlobalFunctionMiddleware } from './globalMiddleware'; +export { createSentryTunnelRoute } from './tunnelRoute'; /** * A no-op stub of the browser tracing integration for the server. Router setup code is shared between client and server, diff --git a/packages/tanstackstart-react/src/server/tunnelRoute.ts b/packages/tanstackstart-react/src/server/tunnelRoute.ts new file mode 100644 index 000000000000..44ff32934a56 --- /dev/null +++ b/packages/tanstackstart-react/src/server/tunnelRoute.ts @@ -0,0 +1,43 @@ +import { handleTunnelRequest } from '@sentry/core'; + +export interface CreateSentryTunnelRouteOptions { + allowedDsns: string[]; +} + +type SentryTunnelRouteHandlerContext = { + request: Request; +}; + +type SentryTunnelRoute = { + handlers: { + POST: (context: SentryTunnelRouteHandlerContext) => Promise; + }; +}; + +/** + * Creates a TanStack Start server route configuration for tunneling Sentry envelopes. + * + * @example + * ```ts + * import { createFileRoute } from '@tanstack/react-router'; + * import * as Sentry from '@sentry/tanstackstart-react'; + * + * export const Route = createFileRoute('/monitoring')({ + * server: Sentry.createSentryTunnelRoute({ + * allowedDsns: ['https://public@o0.ingest.sentry.io/0'], + * }), + * }); + * ``` + */ +export function createSentryTunnelRoute(options: CreateSentryTunnelRouteOptions): SentryTunnelRoute { + return { + handlers: { + POST: async ({ request }) => { + return handleTunnelRequest({ + request, + allowedDsns: options.allowedDsns, + }); + }, + }, + }; +} diff --git a/packages/tanstackstart-react/test/server/tunnelRoute.test.ts b/packages/tanstackstart-react/test/server/tunnelRoute.test.ts new file mode 100644 index 000000000000..b638b779e39e --- /dev/null +++ b/packages/tanstackstart-react/test/server/tunnelRoute.test.ts @@ -0,0 +1,48 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const handleTunnelRequestSpy = vi.fn(); + +vi.mock('@sentry/core', async importOriginal => { + const original = await importOriginal(); + return { + ...original, + handleTunnelRequest: (...args: unknown[]) => handleTunnelRequestSpy(...args), + }; +}); + +const { createSentryTunnelRoute } = await import('../../src/server/tunnelRoute'); + +describe('createSentryTunnelRoute', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('returns a server route config with only a POST handler', () => { + const route = createSentryTunnelRoute({ + allowedDsns: ['https://public@o0.ingest.sentry.io/0'], + }); + + expect(Object.keys(route.handlers)).toEqual(['POST']); + expect(route.handlers.POST).toBeTypeOf('function'); + }); + + it('forwards the request and allowed DSNs to handleTunnelRequest', async () => { + const request = new Request('http://localhost:3000/monitoring', { method: 'POST', body: 'envelope' }); + const allowedDsns = ['https://public@o0.ingest.sentry.io/0']; + const response = new Response('ok', { status: 200 }); + + handleTunnelRequestSpy.mockResolvedValueOnce(response); + + const route = createSentryTunnelRoute({ allowedDsns }); + const result = await route.handlers.POST({ request }); + + expect(handleTunnelRequestSpy).toHaveBeenCalledTimes(1); + const [options] = handleTunnelRequestSpy.mock.calls[0]!; + expect(options).toEqual({ + request, + allowedDsns, + }); + expect(options.allowedDsns).toBe(allowedDsns); + expect(result).toBe(response); + }); +}); diff --git a/yarn.lock b/yarn.lock index 95937c3d01b6..77a5ac686356 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,25 +2,6 @@ # yarn lockfile v1 -"@actions/artifact@5.0.3": - version "5.0.3" - resolved "https://registry.yarnpkg.com/@actions/artifact/-/artifact-5.0.3.tgz#e3ca31d98a5836c23d4c5b829429b5aa6f71f0ff" - integrity sha512-FIEG8Kum0wABZnktJvFi1xuVPc31xrunhZwLCvjrCGISQOm0ifyo7cjqf6PHiEeqoWMa5HIGOsB+lGM4aKCseA== - dependencies: - "@actions/core" "^2.0.0" - "@actions/github" "^6.0.1" - "@actions/http-client" "^3.0.2" - "@azure/storage-blob" "^12.29.1" - "@octokit/core" "^5.2.1" - "@octokit/plugin-request-log" "^1.0.4" - "@octokit/plugin-retry" "^3.0.9" - "@octokit/request" "^8.4.1" - "@octokit/request-error" "^5.1.1" - "@protobuf-ts/plugin" "^2.2.3-alpha.1" - archiver "^7.0.1" - jwt-decode "^3.1.2" - unzip-stream "^0.3.1" - "@actions/artifact@^6.1.0": version "6.1.0" resolved "https://registry.yarnpkg.com/@actions/artifact/-/artifact-6.1.0.tgz#6d30eb1837b1f047dce2ebe364aa60a7881f202d" @@ -49,14 +30,6 @@ "@actions/http-client" "^2.0.1" uuid "^8.3.2" -"@actions/core@^2.0.0": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@actions/core/-/core-2.0.3.tgz#b05e8cf407ab393e5d10282357a74e1ee2315eee" - integrity sha512-Od9Thc3T1mQJYddvVPM4QGiLUewdh+3txmDYHHxoNdkqysR1MbCT+rFOtNUxYAz+7+6RIsqipVahY2GJqGPyxA== - dependencies: - "@actions/exec" "^2.0.0" - "@actions/http-client" "^3.0.2" - "@actions/core@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@actions/core/-/core-3.0.0.tgz#89cb07c119e9b46a649ad5f355e77de9b3108cf8" @@ -72,13 +45,6 @@ dependencies: "@actions/io" "^1.0.1" -"@actions/exec@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@actions/exec/-/exec-2.0.0.tgz#35e829723389f80e362ec2cc415697ec74362ad8" - integrity sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw== - dependencies: - "@actions/io" "^2.0.0" - "@actions/exec@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@actions/exec/-/exec-3.0.0.tgz#8c3464d20f0aa4068707757021d7e3c01a7ee203" @@ -96,19 +62,6 @@ "@octokit/plugin-paginate-rest" "^2.17.0" "@octokit/plugin-rest-endpoint-methods" "^5.13.0" -"@actions/github@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@actions/github/-/github-6.0.1.tgz#76e5f96df062c90635a7181ef45ff1c4ac21306e" - integrity sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw== - dependencies: - "@actions/http-client" "^2.2.0" - "@octokit/core" "^5.0.1" - "@octokit/plugin-paginate-rest" "^9.2.2" - "@octokit/plugin-rest-endpoint-methods" "^10.4.0" - "@octokit/request" "^8.4.1" - "@octokit/request-error" "^5.1.1" - undici "^5.28.5" - "@actions/github@^9.0.0": version "9.0.0" resolved "https://registry.yarnpkg.com/@actions/github/-/github-9.0.0.tgz#c86dae4128b2a6987271e2663bee9e766464840a" @@ -130,7 +83,7 @@ "@actions/core" "^3.0.0" minimatch "^3.0.4" -"@actions/http-client@^2.0.1", "@actions/http-client@^2.2.0": +"@actions/http-client@^2.0.1": version "2.2.3" resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-2.2.3.tgz#31fc0b25c0e665754ed39a9f19a8611fc6dab674" integrity sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA== @@ -159,11 +112,6 @@ resolved "https://registry.yarnpkg.com/@actions/io/-/io-1.1.3.tgz#4cdb6254da7962b07473ff5c335f3da485d94d71" integrity sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q== -"@actions/io@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@actions/io/-/io-2.0.0.tgz#3ad1271ba3cd515324f2215e8d4c1c0c3864d65b" - integrity sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg== - "@actions/io@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@actions/io/-/io-3.0.2.tgz#6f89b27a159d109836d983efa283997c23b92284" @@ -578,11 +526,6 @@ resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.10.1.tgz#70e45678f06c72fa2e350e8553ec4a4d72b92e06" integrity sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg== -"@assemblyscript/loader@^0.19.21": - version "0.19.23" - resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.19.23.tgz#7fccae28d0a2692869f1d1219d36093bc24d5e72" - integrity sha512-ulkCYfFbYj01ie1MDOyxv2F6SpRN1TOj7fQxbP07D6HmeR+gr2JLSmINKjga2emB+b1L2KGrFKBTc+e00p54nw== - "@astrojs/compiler@^2.3.0": version "2.12.2" resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-2.12.2.tgz#5913b6ec7efffebdfb37fae9a50122802ae08c64" @@ -1581,7 +1524,7 @@ jsonwebtoken "^9.0.0" uuid "^8.3.0" -"@azure/storage-blob@^12.29.1", "@azure/storage-blob@^12.30.0": +"@azure/storage-blob@^12.30.0": version "12.31.0" resolved "https://registry.yarnpkg.com/@azure/storage-blob/-/storage-blob-12.31.0.tgz#97b09be2bf6ab59739b862edd8124798362ce720" integrity sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg== @@ -3097,11 +3040,6 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@colors/colors@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" - integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== - "@colors/colors@1.6.0", "@colors/colors@^1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" @@ -5340,13 +5278,6 @@ semver "^7.5.3" tar "^7.4.0" -"@minimistjs/subarg@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@minimistjs/subarg/-/subarg-1.0.0.tgz#484fdfebda9dc32087d7c7999ec6350684fb42d2" - integrity sha512-Q/ONBiM2zNeYUy0mVSO44mWWKYM3UHuEK43PKIOzJCbvUnPoMH1K+gk3cf1kgnCVJFlWmddahQQCmrmBGlk9jQ== - dependencies: - minimist "^1.1.0" - "@mjackson/node-fetch-server@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@mjackson/node-fetch-server/-/node-fetch-server-0.2.0.tgz#577c0c25d8aae9f69a97738b7b0d03d1471cdc49" @@ -5919,11 +5850,6 @@ dependencies: "@octokit/types" "^6.0.3" -"@octokit/auth-token@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-4.0.0.tgz#40d203ea827b9f17f42a29c6afb93b7745ef80c7" - integrity sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA== - "@octokit/auth-token@^6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-6.0.0.tgz#b02e9c08a2d8937df09a2a981f226ad219174c53" @@ -5942,19 +5868,6 @@ before-after-hook "^2.2.0" universal-user-agent "^6.0.0" -"@octokit/core@^5.0.1", "@octokit/core@^5.2.1": - version "5.2.2" - resolved "https://registry.yarnpkg.com/@octokit/core/-/core-5.2.2.tgz#252805732de9b4e8e4f658d34b80c4c9b2534761" - integrity sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg== - dependencies: - "@octokit/auth-token" "^4.0.0" - "@octokit/graphql" "^7.1.0" - "@octokit/request" "^8.4.1" - "@octokit/request-error" "^5.1.1" - "@octokit/types" "^13.0.0" - before-after-hook "^2.2.0" - universal-user-agent "^6.0.0" - "@octokit/core@^7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@octokit/core/-/core-7.0.6.tgz#0d58704391c6b681dec1117240ea4d2a98ac3916" @@ -5985,14 +5898,6 @@ is-plain-object "^5.0.0" universal-user-agent "^6.0.0" -"@octokit/endpoint@^9.0.6": - version "9.0.6" - resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-9.0.6.tgz#114d912108fe692d8b139cfe7fc0846dfd11b6c0" - integrity sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw== - dependencies: - "@octokit/types" "^13.1.0" - universal-user-agent "^6.0.0" - "@octokit/graphql@^4.5.8": version "4.8.0" resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.8.0.tgz#664d9b11c0e12112cbf78e10f49a05959aa22cc3" @@ -6002,15 +5907,6 @@ "@octokit/types" "^6.0.3" universal-user-agent "^6.0.0" -"@octokit/graphql@^7.1.0": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-7.1.1.tgz#79d9f3d0c96a8fd13d64186fe5c33606d48b79cc" - integrity sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g== - dependencies: - "@octokit/request" "^8.4.1" - "@octokit/types" "^13.0.0" - universal-user-agent "^6.0.0" - "@octokit/graphql@^9.0.3": version "9.0.3" resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-9.0.3.tgz#5b8341c225909e924b466705c13477face869456" @@ -6025,16 +5921,6 @@ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-12.11.0.tgz#da5638d64f2b919bca89ce6602d059f1b52d3ef0" integrity sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ== -"@octokit/openapi-types@^20.0.0": - version "20.0.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-20.0.0.tgz#9ec2daa0090eeb865ee147636e0c00f73790c6e5" - integrity sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA== - -"@octokit/openapi-types@^24.2.0": - version "24.2.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-24.2.0.tgz#3d55c32eac0d38da1a7083a9c3b0cca77924f7d3" - integrity sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg== - "@octokit/openapi-types@^27.0.0": version "27.0.0" resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-27.0.0.tgz#374ea53781965fd02a9d36cacb97e152cefff12d" @@ -6054,30 +5940,11 @@ dependencies: "@octokit/types" "^6.40.0" -"@octokit/plugin-paginate-rest@^9.2.2": - version "9.2.2" - resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz#c516bc498736bcdaa9095b9a1d10d9d0501ae831" - integrity sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ== - dependencies: - "@octokit/types" "^12.6.0" - -"@octokit/plugin-request-log@^1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" - integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== - "@octokit/plugin-request-log@^6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz#de1c1e557df6c08adb631bf78264fa741e01b317" integrity sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q== -"@octokit/plugin-rest-endpoint-methods@^10.4.0": - version "10.4.1" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz#41ba478a558b9f554793075b2e20cd2ef973be17" - integrity sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg== - dependencies: - "@octokit/types" "^12.6.0" - "@octokit/plugin-rest-endpoint-methods@^17.0.0": version "17.0.0" resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz#8c54397d3a4060356a1c8a974191ebf945924105" @@ -6093,14 +5960,6 @@ "@octokit/types" "^6.39.0" deprecation "^2.3.1" -"@octokit/plugin-retry@^3.0.9": - version "3.0.9" - resolved "https://registry.yarnpkg.com/@octokit/plugin-retry/-/plugin-retry-3.0.9.tgz#ae625cca1e42b0253049102acd71c1d5134788fe" - integrity sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ== - dependencies: - "@octokit/types" "^6.0.3" - bottleneck "^2.15.3" - "@octokit/plugin-retry@^8.0.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@octokit/plugin-retry/-/plugin-retry-8.1.0.tgz#e25c2fb5e0a09cfe674ef9df75d7ca4fafa16c11" @@ -6119,15 +5978,6 @@ deprecation "^2.0.0" once "^1.4.0" -"@octokit/request-error@^5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-5.1.1.tgz#b9218f9c1166e68bb4d0c89b638edc62c9334805" - integrity sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g== - dependencies: - "@octokit/types" "^13.1.0" - deprecation "^2.0.0" - once "^1.4.0" - "@octokit/request-error@^7.0.2", "@octokit/request-error@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-7.1.0.tgz#440fa3cae310466889778f5a222b47a580743638" @@ -6158,30 +6008,6 @@ node-fetch "^2.6.7" universal-user-agent "^6.0.0" -"@octokit/request@^8.4.1": - version "8.4.1" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-8.4.1.tgz#715a015ccf993087977ea4365c44791fc4572486" - integrity sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw== - dependencies: - "@octokit/endpoint" "^9.0.6" - "@octokit/request-error" "^5.1.1" - "@octokit/types" "^13.1.0" - universal-user-agent "^6.0.0" - -"@octokit/types@^12.6.0": - version "12.6.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-12.6.0.tgz#8100fb9eeedfe083aae66473bd97b15b62aedcb2" - integrity sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw== - dependencies: - "@octokit/openapi-types" "^20.0.0" - -"@octokit/types@^13.0.0", "@octokit/types@^13.1.0": - version "13.10.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.10.0.tgz#3e7c6b19c0236c270656e4ea666148c2b51fd1a3" - integrity sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA== - dependencies: - "@octokit/openapi-types" "^24.2.0" - "@octokit/types@^16.0.0": version "16.0.0" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-16.0.0.tgz#fbd7fa590c2ef22af881b1d79758bfaa234dbb7c" @@ -11794,35 +11620,6 @@ atomic-sleep@^1.0.0: resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== -autocannon@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/autocannon/-/autocannon-8.0.0.tgz#72b3ade6ec63dca0dc3be157c873d0a27e3f3745" - integrity sha512-fMMcWc2JPFcUaqHeR6+PbmEpTxCrPZyBUM95oG4w3ngJ8NfBNas/ZXA+pTHXLqJ0UlFVTcy05GC25WxKx/M20A== - dependencies: - "@minimistjs/subarg" "^1.0.0" - chalk "^4.1.0" - char-spinner "^1.0.1" - cli-table3 "^0.6.0" - color-support "^1.1.1" - cross-argv "^2.0.0" - form-data "^4.0.0" - has-async-hooks "^1.0.0" - hdr-histogram-js "^3.0.0" - hdr-histogram-percentiles-obj "^3.0.0" - http-parser-js "^0.5.2" - hyperid "^3.0.0" - lodash.chunk "^4.2.0" - lodash.clonedeep "^4.5.0" - lodash.flatten "^4.4.0" - manage-path "^2.0.0" - on-net-listen "^1.1.1" - pretty-bytes "^5.4.1" - progress "^2.0.3" - reinterval "^1.1.0" - retimer "^3.0.0" - semver "^7.3.2" - timestring "^6.0.0" - autoprefixer@^10.4.13, autoprefixer@^10.4.19, autoprefixer@^10.4.21, autoprefixer@^10.4.8: version "10.4.24" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.24.tgz#2c29595f3abd820a79976a609d0bf40eecf212fb" @@ -12977,7 +12774,7 @@ buffer-more-ints@~1.0.0: resolved "https://registry.yarnpkg.com/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz#ef4f8e2dddbad429ed3828a9c55d44f05c611422" integrity sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg== -buffer@^5.2.1, buffer@^5.5.0, buffer@^5.6.0: +buffer@^5.5.0, buffer@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -13328,11 +13125,6 @@ chalk@^5.0.0, chalk@^5.2.0, chalk@^5.3.0: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== -char-spinner@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/char-spinner/-/char-spinner-1.0.1.tgz#e6ea67bd247e107112983b7ab0479ed362800081" - integrity sha512-acv43vqJ0+N0rD+Uw3pDHSxP30FHrywu2NO6/wBaHChJIizpDeBUd6NjqhNhy9LGaEAhZAXn46QzmlAvIWd16g== - character-entities-html4@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" @@ -13523,15 +13315,6 @@ cli-spinners@^2.0.0, cli-spinners@^2.5.0, cli-spinners@^2.9.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.1.tgz#9c0b9dad69a6d47cbb4333c14319b060ed395a35" integrity sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ== -cli-table3@^0.6.0: - version "0.6.5" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f" - integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== - dependencies: - string-width "^4.2.0" - optionalDependencies: - "@colors/colors" "1.5.0" - cli-table@^0.3.1: version "0.3.6" resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.6.tgz#e9d6aa859c7fe636981fd3787378c2a20bce92fc" @@ -13661,7 +13444,7 @@ color-string@^1.6.0, color-string@^1.9.0: color-name "^1.0.0" simple-swizzle "^0.2.2" -color-support@^1.1.1, color-support@^1.1.3: +color-support@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== @@ -14155,11 +13938,6 @@ croner@^9.1.0: resolved "https://registry.yarnpkg.com/croner/-/croner-9.1.0.tgz#94ccbba2570bca329f60f36ec19875dccf9a63aa" integrity sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g== -cross-argv@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/cross-argv/-/cross-argv-2.0.0.tgz#2e7907ba3246f82c967623a3e8525925bbd6c0ad" - integrity sha512-YIaY9TR5Nxeb8SMdtrU8asWVM4jqJDNDYlKV21LxtYcfNJhp1kEsgSa6qXwXgzN0WQWGODps0+TlGp2xQSHwOg== - cross-inspect@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cross-inspect/-/cross-inspect-1.0.1.tgz#15f6f65e4ca963cf4cc1a2b5fef18f6ca328712b" @@ -18602,11 +18380,6 @@ has-ansi@^3.0.0: dependencies: ansi-regex "^3.0.0" -has-async-hooks@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-async-hooks/-/has-async-hooks-1.0.0.tgz#3df965ade8cd2d9dbfdacfbca3e0a5152baaf204" - integrity sha512-YF0VPGjkxr7AyyQQNykX8zK4PvtEDsUJAPqwu06UFz1lb6EvI53sPh5H1kWxg8NXI5LsfRCZ8uX9NkYDZBb/mw== - has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -18818,15 +18591,6 @@ hdr-histogram-js@^2.0.1: base64-js "^1.2.0" pako "^1.0.3" -hdr-histogram-js@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-3.0.1.tgz#b281e90d6ca80ee656bc378dafa39d7239b90855" - integrity sha512-l3GSdZL1Jr1C0kyb461tUjEdrRPZr8Qry7jByltf5JGrA0xvqOSrxRBfcrJqqV/AMEtqqhHhC6w8HW0gn76tRQ== - dependencies: - "@assemblyscript/loader" "^0.19.21" - base64-js "^1.2.0" - pako "^1.0.3" - hdr-histogram-percentiles-obj@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz#9409f4de0c2dda78e61de2d9d78b1e9f3cba283c" @@ -19078,7 +18842,7 @@ http-errors@~1.6.2: setprototypeof "1.1.0" statuses ">= 1.4.0 < 2" -http-parser-js@>=0.5.1, http-parser-js@^0.5.2: +http-parser-js@>=0.5.1: version "0.5.10" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.10.tgz#b3277bd6d7ed5588e20ea73bf724fcbe44609075" integrity sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA== @@ -19197,15 +18961,6 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -hyperid@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/hyperid/-/hyperid-3.3.0.tgz#2042bb296b7f1d5ba0797a5705469af0899c8556" - integrity sha512-7qhCVT4MJIoEsNcbhglhdmBKb09QtcmJNiIQGq7js/Khf5FtQQ9bzcAuloeqBeee7XD7JqDeve9KNlQya5tSGQ== - dependencies: - buffer "^5.2.1" - uuid "^8.3.2" - uuid-parse "^1.1.0" - iconv-lite@0.6.3, iconv-lite@^0.6.2, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" @@ -20580,11 +20335,6 @@ jws@^4.0.0: jwa "^2.0.0" safe-buffer "^5.0.1" -jwt-decode@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" - integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== - jwt-decode@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" @@ -21006,11 +20756,6 @@ lodash.camelcase@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= -lodash.chunk@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc" - integrity sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w== - lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -21046,11 +20791,6 @@ lodash.flatten@^3.0.2: lodash._baseflatten "^3.0.0" lodash._isiterateecall "^3.0.0" -lodash.flatten@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" - integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g== - lodash.foreach@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" @@ -21445,11 +21185,6 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" -manage-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/manage-path/-/manage-path-2.0.0.tgz#f4cf8457b926eeee2a83b173501414bc76eb9597" - integrity sha512-NJhyB+PJYTpxhxZJ3lecIGgh4kwIY2RAh44XvAz9UlqthlQwtPBf62uBVR8XaD8CRuSjQ6TnZH2lNJkbLPZM2A== - map-age-cleaner@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" @@ -22269,7 +22004,7 @@ minimist@^0.2.1: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.2.4.tgz#0085d5501e29033748a2f2a4da0180142697a475" integrity sha512-Pkrrm8NjyQ8yVt8Am9M+yUt74zE3iokhzbG1bFVNjLB92vwM71hf40RkEsryg98BujhVOncKm/C1xROxZ030LQ== -minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -23686,11 +23421,6 @@ on-headers@~1.0.2: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -on-net-listen@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/on-net-listen/-/on-net-listen-1.1.2.tgz#671e55a81c910fa7e5b1e4d506545e9ea0f2e11c" - integrity sha512-y1HRYy8s/RlcBvDUwKXSmkODMdx4KSuIvloCnQYJ2LdBBC1asY4HtfhXwe3UWknLakATZDnbzht2Ijw3M1EqFg== - once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -25419,7 +25149,7 @@ prettier@^3.6.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173" integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg== -pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: +pretty-bytes@^5.3.0: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== @@ -26353,11 +26083,6 @@ rehype@^12.0.1: rehype-stringify "^9.0.0" unified "^10.0.0" -reinterval@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/reinterval/-/reinterval-1.1.0.tgz#3361ecfa3ca6c18283380dd0bb9546f390f5ece7" - integrity sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ== - relateurl@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" @@ -26681,11 +26406,6 @@ retext@^8.1.0: retext-stringify "^3.0.0" unified "^10.0.0" -retimer@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/retimer/-/retimer-3.0.0.tgz#98b751b1feaf1af13eb0228f8ea68b8f9da530df" - integrity sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA== - retry-request@^4.1.1: version "4.1.3" resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-4.1.3.tgz#d5f74daf261372cff58d08b0a1979b4d7cab0fde" @@ -28562,7 +28282,6 @@ stylus@0.59.0, stylus@^0.59.0: sucrase@^3.27.0, sucrase@^3.35.0, sucrase@getsentry/sucrase#es2020-polyfills: version "3.36.0" - uid fd682f6129e507c00bb4e6319cc5d6b767e36061 resolved "https://codeload.github.com/getsentry/sucrase/tar.gz/fd682f6129e507c00bb4e6319cc5d6b767e36061" dependencies: "@jridgewell/gen-mapping" "^0.3.2" @@ -29006,11 +28725,6 @@ tildify@2.0.0: resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a" integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw== -timestring@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/timestring/-/timestring-6.0.0.tgz#b0c7c331981ecf2066ce88bcfb8ee3ae32e7a0f6" - integrity sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA== - tiny-glob@0.2.9, tiny-glob@^0.2.9: version "0.2.9" resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2" @@ -29678,7 +29392,7 @@ undici@7.18.2: resolved "https://registry.yarnpkg.com/undici/-/undici-7.18.2.tgz#6cf724ef799a67d94fd55adf66b1e184176efcdf" integrity sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw== -undici@^5.25.4, undici@^5.28.5: +undici@^5.25.4: version "5.29.0" resolved "https://registry.yarnpkg.com/undici/-/undici-5.29.0.tgz#419595449ae3f2cdcba3580a2e8903399bd1f5a3" integrity sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg== @@ -30189,11 +29903,6 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid-parse@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.1.0.tgz#7061c5a1384ae0e1f943c538094597e1b5f3a65b" - integrity sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A== - uuid-v4@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/uuid-v4/-/uuid-v4-0.1.0.tgz#62d7b310406f6cecfea1528c69f1e8e0bcec5a3a" From 316be89709dfe19862f97ec9d4a2c0c01941ab70 Mon Sep 17 00:00:00 2001 From: Lazar Nikolov Date: Mon, 13 Apr 2026 16:43:25 -0400 Subject: [PATCH 2/7] Add TanStack Start tunnel route e2e coverage --- .../tanstackstart-react/package.json | 14 ++++++++- .../tanstackstart-react/src/globals.d.ts | 2 ++ .../tanstackstart-react/src/router.tsx | 4 +-- .../tanstackstart-react/src/routes/monitor.ts | 13 +++++++++ .../tanstackstart-react/tests/errors.test.ts | 4 +++ .../tests/middleware.test.ts | 4 +++ .../tests/transaction.test.ts | 4 +++ .../tanstackstart-react/tests/tunnel.test.ts | 29 +++++++++++++++++++ .../tanstackstart-react/vite.config.ts | 10 +++++++ 9 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react/src/globals.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/monitor.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json index bcfb3279f684..ee12059975cf 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json @@ -9,8 +9,11 @@ "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:build": "pnpm install && pnpm build", + "test:build:tunnel": "pnpm install && E2E_TEST_USE_TUNNEL_ROUTE=1 pnpm build", "test:build-latest": "pnpm add @tanstack/react-start@latest @tanstack/react-router@latest && pnpm install && pnpm build", - "test:assert": "pnpm test" + "test:assert:proxy": "pnpm test", + "test:assert": "pnpm test:assert:proxy && E2E_TEST_USE_TUNNEL_ROUTE=1 pnpm build && pnpm test:assert:tunnel", + "test:assert:tunnel": "E2E_TEST_USE_TUNNEL_ROUTE=1 pnpm test" }, "dependencies": { "@sentry/tanstackstart-react": "latest || *", @@ -35,5 +38,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "label": "tunnel", + "build-command": "pnpm test:build:tunnel", + "assert-command": "pnpm test:assert:tunnel" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/globals.d.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/globals.d.ts new file mode 100644 index 000000000000..d22cc4c9dd9d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/globals.d.ts @@ -0,0 +1,2 @@ +declare const __APP_DSN__: string; +declare const __APP_TUNNEL__: string; diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/router.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/router.tsx index b1c6f7727a26..9a39b6f35c42 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/router.tsx +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/router.tsx @@ -11,13 +11,13 @@ export const getRouter = () => { if (!router.isServer) { Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions - dsn: 'https://public@dsn.ingest.sentry.io/1337', + dsn: __APP_DSN__, integrations: [Sentry.tanstackRouterBrowserTracingIntegration(router)], // We recommend adjusting this value in production, or using tracesSampler // for finer control tracesSampleRate: 1.0, release: 'e2e-test', - tunnel: 'http://localhost:3031/', // proxy server + tunnel: __APP_TUNNEL__, }); } diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/monitor.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/monitor.ts new file mode 100644 index 000000000000..303cee2a04c1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/monitor.ts @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/tanstackstart-react'; +import { createFileRoute } from '@tanstack/react-router'; + +const USE_TUNNEL_ROUTE = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1'; + +const DEFAULT_DSN = 'https://public@dsn.ingest.sentry.io/1337'; +const TUNNEL_DSN = 'http://public@localhost:3031/1337'; + +export const Route = createFileRoute('/monitor')({ + server: Sentry.createSentryTunnelRoute({ + allowedDsns: [USE_TUNNEL_ROUTE ? TUNNEL_DSN : DEFAULT_DSN], + }), +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts index 04d93e550824..58797757107d 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts @@ -1,6 +1,10 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +const useTunnelRoute = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1'; + +test.skip(useTunnelRoute, 'Default e2e suites run only in the proxy variant'); + test('Sends client-side error to Sentry with auto-instrumentation', async ({ page }) => { const errorEventPromise = waitForError('tanstackstart-react', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'Sentry Client Test Error'; diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/middleware.test.ts index dffab8ea2aa3..e64ae941d2da 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/middleware.test.ts @@ -1,6 +1,10 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; +const useTunnelRoute = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1'; + +test.skip(useTunnelRoute, 'Default e2e suites run only in the proxy variant'); + test('Sends spans for multiple middlewares and verifies they are siblings under the same parent span', async ({ page, }) => { diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts index 5186514d277a..97d516d9fd30 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts @@ -1,6 +1,10 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; +const useTunnelRoute = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1'; + +test.skip(useTunnelRoute, 'Default e2e suites run only in the proxy variant'); + test('Sends a server function transaction with auto-instrumentation', async ({ page }) => { const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => { return ( diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts new file mode 100644 index 000000000000..db911825da25 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +const useTunnelRoute = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1'; + +test.skip(!useTunnelRoute, 'Tunnel assertions only run in the tunnel variant'); + +test('Sends client-side errors through the monitor tunnel route', async ({ page }) => { + const errorEventPromise = waitForError('tanstackstart-react', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Sentry Client Test Error'; + }); + + await page.goto('/'); + + await expect(page.locator('button').filter({ hasText: 'Break the client' })).toBeVisible(); + + const monitorResponsePromise = page.waitForResponse(response => { + return response.url().endsWith('/monitor') && response.request().method() === 'POST'; + }); + + await page.locator('button').filter({ hasText: 'Break the client' }).click(); + + const monitorResponse = await monitorResponsePromise; + const errorEvent = await errorEventPromise; + + expect(monitorResponse.status()).toBe(200); + expect(errorEvent.exception?.values?.[0]?.value).toBe('Sentry Client Test Error'); + expect(errorEvent.transaction).toBe('/'); +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/vite.config.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/vite.config.ts index a2b39609717d..6dffa8366b59 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/vite.config.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/vite.config.ts @@ -5,10 +5,20 @@ import viteReact from '@vitejs/plugin-react-swc'; import { nitro } from 'nitro/vite'; import { sentryTanstackStart } from '@sentry/tanstackstart-react/vite'; +const useTunnelRoute = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1'; + +const appDsn = useTunnelRoute ? 'http://public@localhost:3031/1337' : 'https://public@dsn.ingest.sentry.io/1337'; + +const appTunnel = useTunnelRoute ? '/monitor' : 'http://localhost:3031/'; + export default defineConfig({ server: { port: 3000, }, + define: { + __APP_DSN__: JSON.stringify(appDsn), + __APP_TUNNEL__: JSON.stringify(appTunnel), + }, plugins: [ tsConfigPaths(), tanstackStart(), From 08b415402b6172fbaffeb59ac2d6279777c50158 Mon Sep 17 00:00:00 2001 From: Lazar Nikolov Date: Wed, 15 Apr 2026 17:23:10 -0400 Subject: [PATCH 3/7] Refactor TanStack Start e2e app to cover managed tunnel route variants --- .../tanstackstart-react/package.json | 26 ++- .../tanstackstart-react/src/globals.d.ts | 2 +- .../src/routes/custom-monitor.ts | 18 ++ .../tanstackstart-react/src/routes/monitor.ts | 13 -- .../tanstackstart-react/tests/errors.test.ts | 5 +- .../tests/middleware.test.ts | 5 +- .../tests/transaction.test.ts | 5 +- .../tanstackstart-react/tests/tunnel.test.ts | 31 ++- .../tanstackstart-react/vite.config.ts | 37 +++- .../tanstackstart-react/src/client/sdk.ts | 2 + .../src/client/tunnelRoute.ts | 37 ++++ .../tanstackstart-react/src/vite/index.ts | 1 + .../src/vite/sentryTanstackStart.ts | 26 ++- .../src/vite/tunnelRoute.ts | 203 ++++++++++++++++++ .../test/client/sdk.test.ts | 11 + .../test/client/tunnelRoute.test.ts | 59 +++++ .../test/vite/sentryTanstackStart.test.ts | 49 ++++- .../test/vite/tunnelRoute.test.ts | 117 ++++++++++ 18 files changed, 601 insertions(+), 46 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/custom-monitor.ts delete mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/monitor.ts create mode 100644 packages/tanstackstart-react/src/client/tunnelRoute.ts create mode 100644 packages/tanstackstart-react/src/vite/tunnelRoute.ts create mode 100644 packages/tanstackstart-react/test/client/tunnelRoute.test.ts create mode 100644 packages/tanstackstart-react/test/vite/tunnelRoute.test.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json index ee12059975cf..1b6d9761fc94 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json @@ -9,11 +9,15 @@ "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:build": "pnpm install && pnpm build", - "test:build:tunnel": "pnpm install && E2E_TEST_USE_TUNNEL_ROUTE=1 pnpm build", + "test:build:tunnel-generated": "pnpm install && E2E_TEST_TUNNEL_ROUTE_MODE=dynamic pnpm build", + "test:build:tunnel-static": "pnpm install && E2E_TEST_TUNNEL_ROUTE_MODE=static pnpm build", + "test:build:tunnel-custom": "pnpm install && E2E_TEST_CUSTOM_TUNNEL_ROUTE=1 pnpm build", "test:build-latest": "pnpm add @tanstack/react-start@latest @tanstack/react-router@latest && pnpm install && pnpm build", "test:assert:proxy": "pnpm test", - "test:assert": "pnpm test:assert:proxy && E2E_TEST_USE_TUNNEL_ROUTE=1 pnpm build && pnpm test:assert:tunnel", - "test:assert:tunnel": "E2E_TEST_USE_TUNNEL_ROUTE=1 pnpm test" + "test:assert": "pnpm test:assert:proxy && E2E_TEST_TUNNEL_ROUTE_MODE=dynamic pnpm build && pnpm test:assert:tunnel-generated && E2E_TEST_TUNNEL_ROUTE_MODE=static pnpm build && pnpm test:assert:tunnel-static && E2E_TEST_CUSTOM_TUNNEL_ROUTE=1 pnpm build && pnpm test:assert:tunnel-custom", + "test:assert:tunnel-generated": "E2E_TEST_TUNNEL_ROUTE_MODE=dynamic pnpm test", + "test:assert:tunnel-static": "E2E_TEST_TUNNEL_ROUTE_MODE=static pnpm test", + "test:assert:tunnel-custom": "E2E_TEST_CUSTOM_TUNNEL_ROUTE=1 pnpm test" }, "dependencies": { "@sentry/tanstackstart-react": "latest || *", @@ -42,9 +46,19 @@ "sentryTest": { "variants": [ { - "label": "tunnel", - "build-command": "pnpm test:build:tunnel", - "assert-command": "pnpm test:assert:tunnel" + "label": "tunnel-generated", + "build-command": "pnpm test:build:tunnel-generated", + "assert-command": "pnpm test:assert:tunnel-generated" + }, + { + "label": "tunnel-static", + "build-command": "pnpm test:build:tunnel-static", + "assert-command": "pnpm test:assert:tunnel-static" + }, + { + "label": "tunnel-custom", + "build-command": "pnpm test:build:tunnel-custom", + "assert-command": "pnpm test:assert:tunnel-custom" } ] } diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/globals.d.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/globals.d.ts index d22cc4c9dd9d..6e7d31c7a4e6 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/globals.d.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/globals.d.ts @@ -1,2 +1,2 @@ declare const __APP_DSN__: string; -declare const __APP_TUNNEL__: string; +declare const __APP_TUNNEL__: string | undefined; diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/custom-monitor.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/custom-monitor.ts new file mode 100644 index 000000000000..dd069a821059 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/custom-monitor.ts @@ -0,0 +1,18 @@ +import * as Sentry from "@sentry/tanstackstart-react"; +import { createFileRoute } from "@tanstack/react-router"; + +const USE_CUSTOM_TUNNEL_ROUTE = + process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === "1"; + +const DEFAULT_DSN = "https://public@dsn.ingest.sentry.io/1337"; +const TUNNEL_DSN = "http://public@localhost:3031/1337"; + +// Example of a manually defined tunnel endpoint without relying on the +// managed route injected by `sentryTanstackStart({ tunnelRoute: ... })`. +// If you use a custom route like this one, set `tunnel: '/custom-monitor'` in the client SDK's +// `Sentry.init()` call so browser events are sent to the same endpoint. +export const Route = createFileRoute("/custom-monitor")({ + server: Sentry.createSentryTunnelRoute({ + allowedDsns: [USE_CUSTOM_TUNNEL_ROUTE ? TUNNEL_DSN : DEFAULT_DSN], + }), +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/monitor.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/monitor.ts deleted file mode 100644 index 303cee2a04c1..000000000000 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/monitor.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as Sentry from '@sentry/tanstackstart-react'; -import { createFileRoute } from '@tanstack/react-router'; - -const USE_TUNNEL_ROUTE = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1'; - -const DEFAULT_DSN = 'https://public@dsn.ingest.sentry.io/1337'; -const TUNNEL_DSN = 'http://public@localhost:3031/1337'; - -export const Route = createFileRoute('/monitor')({ - server: Sentry.createSentryTunnelRoute({ - allowedDsns: [USE_TUNNEL_ROUTE ? TUNNEL_DSN : DEFAULT_DSN], - }), -}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts index 58797757107d..a49b77a293b1 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts @@ -1,9 +1,10 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; -const useTunnelRoute = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1'; +const usesManagedTunnelRoute = + (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1'; -test.skip(useTunnelRoute, 'Default e2e suites run only in the proxy variant'); +test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant'); test('Sends client-side error to Sentry with auto-instrumentation', async ({ page }) => { const errorEventPromise = waitForError('tanstackstart-react', errorEvent => { diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/middleware.test.ts index e64ae941d2da..ab31ce5e022a 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/middleware.test.ts @@ -1,9 +1,10 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -const useTunnelRoute = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1'; +const usesManagedTunnelRoute = + (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1'; -test.skip(useTunnelRoute, 'Default e2e suites run only in the proxy variant'); +test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant'); test('Sends spans for multiple middlewares and verifies they are siblings under the same parent span', async ({ page, diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts index 97d516d9fd30..f5e70a676432 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/transaction.test.ts @@ -1,9 +1,10 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -const useTunnelRoute = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1'; +const usesManagedTunnelRoute = + (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1'; -test.skip(useTunnelRoute, 'Default e2e suites run only in the proxy variant'); +test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant'); test('Sends a server function transaction with auto-instrumentation', async ({ page }) => { const transactionEventPromise = waitForTransaction('tanstackstart-react', transactionEvent => { diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts index db911825da25..1a585446c0f4 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts @@ -1,29 +1,46 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/test-utils'; -const useTunnelRoute = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1'; +const tunnelRouteMode = + process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? + (process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1' ? 'custom' : 'off'); -test.skip(!useTunnelRoute, 'Tunnel assertions only run in the tunnel variant'); +test.skip(tunnelRouteMode === 'off', 'Tunnel assertions only run in the tunnel-route variants'); -test('Sends client-side errors through the monitor tunnel route', async ({ page }) => { +test('Sends client-side errors through the configured tunnel route', async ({ page }) => { const errorEventPromise = waitForError('tanstackstart-react', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'Sentry Client Test Error'; }); await page.goto('/'); + const pageOrigin = new URL(page.url()).origin; await expect(page.locator('button').filter({ hasText: 'Break the client' })).toBeVisible(); - const monitorResponsePromise = page.waitForResponse(response => { - return response.url().endsWith('/monitor') && response.request().method() === 'POST'; + const managedTunnelResponsePromise = page.waitForResponse(response => { + const responseUrl = new URL(response.url()); + + return responseUrl.origin === pageOrigin && response.request().method() === 'POST'; }); await page.locator('button').filter({ hasText: 'Break the client' }).click(); - const monitorResponse = await monitorResponsePromise; + const managedTunnelResponse = await managedTunnelResponsePromise; + const managedTunnelUrl = new URL(managedTunnelResponse.url()); const errorEvent = await errorEventPromise; - expect(monitorResponse.status()).toBe(200); + expect(managedTunnelResponse.status()).toBe(200); + expect(managedTunnelUrl.origin).toBe(pageOrigin); + + if (tunnelRouteMode === 'static') { + expect(managedTunnelUrl.pathname).toBe('/monitor'); + } else if (tunnelRouteMode === 'custom') { + expect(managedTunnelUrl.pathname).toBe('/custom-monitor'); + } else { + expect(managedTunnelUrl.pathname).toMatch(/^\/[a-z0-9]{8}$/); + expect(managedTunnelUrl.pathname).not.toBe('/monitor'); + } + expect(errorEvent.exception?.values?.[0]?.value).toBe('Sentry Client Test Error'); expect(errorEvent.transaction).toBe('/'); }); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/vite.config.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/vite.config.ts index 6dffa8366b59..8ee4c5ce28a2 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/vite.config.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/vite.config.ts @@ -1,15 +1,30 @@ -import { defineConfig } from 'vite'; -import tsConfigPaths from 'vite-tsconfig-paths'; -import { tanstackStart } from '@tanstack/react-start/plugin/vite'; -import viteReact from '@vitejs/plugin-react-swc'; -import { nitro } from 'nitro/vite'; -import { sentryTanstackStart } from '@sentry/tanstackstart-react/vite'; +import { defineConfig } from "vite"; +import tsConfigPaths from "vite-tsconfig-paths"; +import { tanstackStart } from "@tanstack/react-start/plugin/vite"; +import viteReact from "@vitejs/plugin-react-swc"; +import { nitro } from "nitro/vite"; +import { sentryTanstackStart } from "@sentry/tanstackstart-react/vite"; -const useTunnelRoute = process.env.E2E_TEST_USE_TUNNEL_ROUTE === '1'; +const tunnelRouteMode = process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? "off"; +const useManagedTunnelRoute = tunnelRouteMode !== "off"; +const useCustomTunnelRoute = process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === "1"; -const appDsn = useTunnelRoute ? 'http://public@localhost:3031/1337' : 'https://public@dsn.ingest.sentry.io/1337'; +const appDsn = useManagedTunnelRoute || useCustomTunnelRoute + ? "http://public@localhost:3031/1337" + : "https://public@dsn.ingest.sentry.io/1337"; -const appTunnel = useTunnelRoute ? '/monitor' : 'http://localhost:3031/'; +const appTunnel = useManagedTunnelRoute + ? undefined + : useCustomTunnelRoute + ? "/custom-monitor" + : "http://localhost:3031/"; + +const tunnelRoute = + tunnelRouteMode === "dynamic" + ? { allowedDsns: [appDsn], tunnel: true as const } + : tunnelRouteMode === "static" + ? { allowedDsns: [appDsn], tunnel: "/monitor" } + : undefined; export default defineConfig({ server: { @@ -17,7 +32,8 @@ export default defineConfig({ }, define: { __APP_DSN__: JSON.stringify(appDsn), - __APP_TUNNEL__: JSON.stringify(appTunnel), + __APP_TUNNEL__: + appTunnel === undefined ? "undefined" : JSON.stringify(appTunnel), }, plugins: [ tsConfigPaths(), @@ -30,6 +46,7 @@ export default defineConfig({ project: process.env.E2E_TEST_SENTRY_PROJECT, authToken: process.env.E2E_TEST_AUTH_TOKEN, debug: true, + tunnelRoute, }), ], }); diff --git a/packages/tanstackstart-react/src/client/sdk.ts b/packages/tanstackstart-react/src/client/sdk.ts index b0ee3b53053f..0998c027f112 100644 --- a/packages/tanstackstart-react/src/client/sdk.ts +++ b/packages/tanstackstart-react/src/client/sdk.ts @@ -2,6 +2,7 @@ import type { Client } from '@sentry/core'; import { applySdkMetadata } from '@sentry/core'; import type { BrowserOptions as ReactBrowserOptions } from '@sentry/react'; import { getDefaultIntegrations as getReactDefaultIntegrations, init as initReactSDK } from '@sentry/react'; +import { applyTunnelRouteOption } from './tunnelRoute'; /** * Initializes the TanStack Start React SDK @@ -14,6 +15,7 @@ export function init(options: ReactBrowserOptions): Client | undefined { ...options, }; + applyTunnelRouteOption(sentryOptions); applySdkMetadata(sentryOptions, 'tanstackstart-react', ['tanstackstart-react', 'react']); return initReactSDK(sentryOptions); diff --git a/packages/tanstackstart-react/src/client/tunnelRoute.ts b/packages/tanstackstart-react/src/client/tunnelRoute.ts new file mode 100644 index 000000000000..8817a6637f55 --- /dev/null +++ b/packages/tanstackstart-react/src/client/tunnelRoute.ts @@ -0,0 +1,37 @@ +import { consoleSandbox } from '@sentry/core'; +import type { BrowserOptions as ReactBrowserOptions } from '@sentry/react'; + +declare const __SENTRY_TANSTACKSTART_TUNNEL_ROUTE__: string | undefined; + +let hasWarnedAboutManagedTunnelRouteOverride = false; + +/** + * Applies the managed tunnel route from `sentryTanstackStart({ tunnelRoute: ... })` unless the user already + * configured an explicit runtime `tunnel` option in `Sentry.init()`. + */ +export function applyTunnelRouteOption(options: ReactBrowserOptions): void { + const managedTunnelRoute = + typeof __SENTRY_TANSTACKSTART_TUNNEL_ROUTE__ !== 'undefined' + ? __SENTRY_TANSTACKSTART_TUNNEL_ROUTE__ + : undefined; + + if (!managedTunnelRoute) { + return; + } + + if (options.tunnel) { + if (!hasWarnedAboutManagedTunnelRouteOverride) { + hasWarnedAboutManagedTunnelRouteOverride = true; + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/tanstackstart-react] `Sentry.init({ tunnel: ... })` overrides the managed `sentryTanstackStart({ tunnelRoute: ... })` route. Remove the runtime `tunnel` option if you want the managed tunnel route to be used.', + ); + }); + } + + return; + } + + options.tunnel = managedTunnelRoute; +} diff --git a/packages/tanstackstart-react/src/vite/index.ts b/packages/tanstackstart-react/src/vite/index.ts index 85143344028d..b7f65e26f5d2 100644 --- a/packages/tanstackstart-react/src/vite/index.ts +++ b/packages/tanstackstart-react/src/vite/index.ts @@ -1,2 +1,3 @@ export { sentryTanstackStart } from './sentryTanstackStart'; export type { SentryTanstackStartOptions } from './sentryTanstackStart'; +export type { TunnelRouteOptions } from './tunnelRoute'; diff --git a/packages/tanstackstart-react/src/vite/sentryTanstackStart.ts b/packages/tanstackstart-react/src/vite/sentryTanstackStart.ts index fd5d5b2f0d05..17cbc1b465a1 100644 --- a/packages/tanstackstart-react/src/vite/sentryTanstackStart.ts +++ b/packages/tanstackstart-react/src/vite/sentryTanstackStart.ts @@ -2,6 +2,8 @@ import type { BuildTimeOptionsBase } from '@sentry/core'; import type { Plugin } from 'vite'; import { makeAutoInstrumentMiddlewarePlugin } from './autoInstrumentMiddleware'; import { makeAddSentryVitePlugin, makeEnableSourceMapsVitePlugin } from './sourceMaps'; +import type { TunnelRouteOptions } from './tunnelRoute'; +import { makeTunnelRoutePlugin } from './tunnelRoute'; /** * Build-time options for the Sentry TanStack Start SDK. @@ -19,6 +21,18 @@ export interface SentryTanstackStartOptions extends BuildTimeOptionsBase { * @default true */ autoInstrumentMiddleware?: boolean; + + /** + * Configures a framework-managed same-origin tunnel route for Sentry envelopes. + * + * This creates a TanStack Start server route backed by `createSentryTunnelRoute()` and applies the resulting path + * as the default `tunnel` option on the client. Use `tunnel: true` to generate an opaque route path per dev session + * or production build, or provide a static absolute path string to control the route name yourself. + * + * If you also pass `tunnel` to `Sentry.init()`, that explicit runtime option wins and a warning is emitted because + * the managed tunnel route is being bypassed. + */ + tunnelRoute?: TunnelRouteOptions; } /** @@ -46,13 +60,21 @@ export interface SentryTanstackStartOptions extends BuildTimeOptionsBase { * @returns An array of Vite plugins */ export function sentryTanstackStart(options: SentryTanstackStartOptions = {}): Plugin[] { - // only add plugins in production builds + const tunnelRoutePlugin = options.tunnelRoute + ? makeTunnelRoutePlugin(options.tunnelRoute, options.debug) + : undefined; + + // only add build-time plugins in production builds if (process.env.NODE_ENV === 'development') { - return []; + return tunnelRoutePlugin ? [tunnelRoutePlugin] : []; } const plugins: Plugin[] = [...makeAddSentryVitePlugin(options)]; + if (tunnelRoutePlugin) { + plugins.push(tunnelRoutePlugin); + } + // middleware auto-instrumentation if (options.autoInstrumentMiddleware !== false) { plugins.push(makeAutoInstrumentMiddlewarePlugin({ enabled: true, debug: options.debug })); diff --git a/packages/tanstackstart-react/src/vite/tunnelRoute.ts b/packages/tanstackstart-react/src/vite/tunnelRoute.ts new file mode 100644 index 000000000000..aef2b8933f44 --- /dev/null +++ b/packages/tanstackstart-react/src/vite/tunnelRoute.ts @@ -0,0 +1,203 @@ +import type { Plugin } from "vite"; + +export interface TunnelRouteOptions { + /** + * A list of DSNs that are allowed to use the managed tunnel route. + */ + allowedDsns: string[]; + + /** + * Controls the public route path used by the managed tunnel route. + * + * - `true` generates an opaque path once per dev session or production build. + * - `'/custom-path'` uses a fixed absolute route path. + * + * @default true + */ + tunnel?: true | string; +} + +const MANAGED_TUNNEL_ROUTE_IMPORT = "SentryManagedTunnelRouteImport"; +const MANAGED_TUNNEL_ROUTE_NAME = "SentryManagedTunnelRoute"; +const MANAGED_TUNNEL_ROUTE_PATH_ENV_KEY = + "__SENTRY_TANSTACKSTART_TUNNEL_ROUTE__"; + +const VIRTUAL_TUNNEL_ROUTE_ID = + "virtual:sentry-tanstackstart-react/tunnel-route"; +const RESOLVED_VIRTUAL_TUNNEL_ROUTE_ID = `\0${VIRTUAL_TUNNEL_ROUTE_ID}`; + +function generateRandomTunnelRoute(): string { + return `/${Math.random().toString(36).substring(2, 10)}`; +} + +export function resolveTunnelRoute(tunnel: true | string): string { + if (typeof tunnel === "string") { + return tunnel; + } + + if (process.env[MANAGED_TUNNEL_ROUTE_PATH_ENV_KEY]) { + return process.env[MANAGED_TUNNEL_ROUTE_PATH_ENV_KEY]; + } + + const resolvedTunnelRoute = generateRandomTunnelRoute(); + process.env[MANAGED_TUNNEL_ROUTE_PATH_ENV_KEY] = resolvedTunnelRoute; + return resolvedTunnelRoute; +} + +function validateTunnelRouteOptions(options: TunnelRouteOptions): string { + if (options.allowedDsns.length === 0) { + throw new Error( + "[@sentry/tanstackstart-react] `sentryTanstackStart({ tunnelRoute })` requires at least one allowed DSN.", + ); + } + + const tunnelRoute = options.tunnel ?? true; + + if ( + typeof tunnelRoute === "string" && + (!tunnelRoute.startsWith("/") || + tunnelRoute.includes("?") || + tunnelRoute.includes("#")) + ) { + throw new Error( + "[@sentry/tanstackstart-react] `tunnelRoute.tunnel` must be `true` or an absolute route path starting with `/` and without query or hash segments.", + ); + } + + return resolveTunnelRoute(tunnelRoute); +} + +function hasRouteConflict( + source: string, + resolvedTunnelRoute: string, +): boolean { + return ( + source.includes(`fullPath: '${resolvedTunnelRoute}'`) || + source.includes(`path: '${resolvedTunnelRoute}'`) || + source.includes(`id: '${resolvedTunnelRoute}'`) + ); +} + +function injectAfterLastImport(source: string, statement: string): string { + const importMatches = [...source.matchAll(/^import .+$/gm)]; + const lastImport = importMatches.at(-1); + + if (!lastImport || lastImport.index === undefined) { + throw new Error( + "[@sentry/tanstackstart-react] Failed to inject the managed tunnel route because `routeTree.gen.ts` imports could not be located.", + ); + } + + const insertIndex = lastImport.index + lastImport[0].length; + return `${source.slice(0, insertIndex)}\n${statement}${source.slice(insertIndex)}`; +} + +export function injectManagedTunnelRoute( + source: string, + resolvedTunnelRoute: string, +): string { + if (source.includes(VIRTUAL_TUNNEL_ROUTE_ID)) { + return source; + } + + if (hasRouteConflict(source, resolvedTunnelRoute)) { + throw new Error( + `[@sentry/tanstackstart-react] Cannot register managed tunnel route "${resolvedTunnelRoute}" because an existing TanStack Start route already uses that path.`, + ); + } + + const serializedTunnelRoute = JSON.stringify(resolvedTunnelRoute); + + let transformedSource = injectAfterLastImport( + source, + `import { Route as ${MANAGED_TUNNEL_ROUTE_IMPORT} } from '${VIRTUAL_TUNNEL_ROUTE_ID}'`, + ); + + const rootRouteChildrenMatch = transformedSource.match( + /const rootRouteChildren(?:\s*:\s*RootRouteChildren)?\s*=\s*\{/, + ); + + if (!rootRouteChildrenMatch || rootRouteChildrenMatch.index === undefined) { + throw new Error( + "[@sentry/tanstackstart-react] Failed to inject the managed tunnel route because the generated TanStack route tree did not contain `rootRouteChildren`.", + ); + } + + const injectedRootRouteChildrenDeclaration = `const ${MANAGED_TUNNEL_ROUTE_NAME} = ${MANAGED_TUNNEL_ROUTE_IMPORT}.update({ + id: ${serializedTunnelRoute}, + path: ${serializedTunnelRoute}, + getParentRoute: () => rootRouteImport, +} as any) + +${rootRouteChildrenMatch[0]} + ${MANAGED_TUNNEL_ROUTE_NAME}: ${MANAGED_TUNNEL_ROUTE_NAME}, +`; + + transformedSource = `${transformedSource.slice(0, rootRouteChildrenMatch.index)}${injectedRootRouteChildrenDeclaration}${transformedSource.slice(rootRouteChildrenMatch.index + rootRouteChildrenMatch[0].length)}`; + + return transformedSource; +} + +export function makeTunnelRoutePlugin( + options: TunnelRouteOptions, + debug?: boolean, +): Plugin { + const resolvedTunnelRoute = validateTunnelRouteOptions(options); + const serializedTunnelRoute = JSON.stringify(resolvedTunnelRoute); + const serializedAllowedDsns = JSON.stringify(options.allowedDsns); + + if (debug) { + // eslint-disable-next-line no-console + console.log( + `[@sentry/tanstackstart-react] Registered tunnel route: ${resolvedTunnelRoute}`, + ); + } + + return { + name: "sentry-tanstackstart-tunnel-route", + enforce: "pre", + config() { + return { + define: { + __SENTRY_TANSTACKSTART_TUNNEL_ROUTE__: serializedTunnelRoute, + }, + }; + }, + resolveId(source) { + return source === VIRTUAL_TUNNEL_ROUTE_ID + ? RESOLVED_VIRTUAL_TUNNEL_ROUTE_ID + : null; + }, + load(id) { + if (id !== RESOLVED_VIRTUAL_TUNNEL_ROUTE_ID) { + return null; + } + + return `import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute(${serializedTunnelRoute})({ + server: { + handlers: { + async POST({ request }) { + const Sentry = await import('@sentry/tanstackstart-react'); + return Sentry.createSentryTunnelRoute({ + allowedDsns: ${serializedAllowedDsns}, + }).handlers.POST({ request }); + }, + }, + }, +}); +`; + }, + transform(source, id) { + if ( + !id.endsWith("/routeTree.gen.ts") && + !id.endsWith("\\routeTree.gen.ts") + ) { + return null; + } + + return injectManagedTunnelRoute(source, resolvedTunnelRoute); + }, + }; +} diff --git a/packages/tanstackstart-react/test/client/sdk.test.ts b/packages/tanstackstart-react/test/client/sdk.test.ts index 4cba4a199ef5..400bbe877dc1 100644 --- a/packages/tanstackstart-react/test/client/sdk.test.ts +++ b/packages/tanstackstart-react/test/client/sdk.test.ts @@ -9,6 +9,7 @@ describe('TanStack Start React Client SDK', () => { describe('init', () => { beforeEach(() => { vi.clearAllMocks(); + vi.unstubAllGlobals(); }); it('Adds TanStack Start React client metadata to the SDK options', () => { @@ -41,5 +42,15 @@ describe('TanStack Start React Client SDK', () => { it('returns client from init', () => { expect(init({})).not.toBeUndefined(); }); + + it('applies the managed tunnel route when no runtime tunnel is provided', () => { + vi.stubGlobal('__SENTRY_TANSTACKSTART_TUNNEL_ROUTE__', '/managed-tunnel'); + + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }); + + expect(reactInit).toHaveBeenLastCalledWith(expect.objectContaining({ tunnel: '/managed-tunnel' })); + }); }); }); diff --git a/packages/tanstackstart-react/test/client/tunnelRoute.test.ts b/packages/tanstackstart-react/test/client/tunnelRoute.test.ts new file mode 100644 index 000000000000..90b91481305b --- /dev/null +++ b/packages/tanstackstart-react/test/client/tunnelRoute.test.ts @@ -0,0 +1,59 @@ +import type { BrowserOptions } from '@sentry/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +describe('applyTunnelRouteOption()', () => { + beforeEach(() => { + vi.resetModules(); + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('applies the managed tunnel route when no runtime tunnel is set', async () => { + vi.stubGlobal('__SENTRY_TANSTACKSTART_TUNNEL_ROUTE__', '/managed-tunnel'); + + const { applyTunnelRouteOption } = await import('../../src/client/tunnelRoute'); + + const options: BrowserOptions = { + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }; + + applyTunnelRouteOption(options); + + expect(options.tunnel).toBe('/managed-tunnel'); + }); + + it('does not override an explicit runtime tunnel and warns instead', async () => { + vi.stubGlobal('__SENTRY_TANSTACKSTART_TUNNEL_ROUTE__', '/managed-tunnel'); + + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const { applyTunnelRouteOption } = await import('../../src/client/tunnelRoute'); + + const options: BrowserOptions = { + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tunnel: '/runtime-tunnel', + }; + + applyTunnelRouteOption(options); + + expect(options.tunnel).toBe('/runtime-tunnel'); + expect(warnSpy).toHaveBeenCalledWith( + '[@sentry/tanstackstart-react] `Sentry.init({ tunnel: ... })` overrides the managed `sentryTanstackStart({ tunnelRoute: ... })` route. Remove the runtime `tunnel` option if you want the managed tunnel route to be used.', + ); + }); + + it('does nothing when no managed tunnel route was injected', async () => { + const { applyTunnelRouteOption } = await import('../../src/client/tunnelRoute'); + + const options: BrowserOptions = { + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }; + + applyTunnelRouteOption(options); + + expect(options.tunnel).toBeUndefined(); + }); +}); diff --git a/packages/tanstackstart-react/test/vite/sentryTanstackStart.test.ts b/packages/tanstackstart-react/test/vite/sentryTanstackStart.test.ts index ef18da74d03a..004251c63b66 100644 --- a/packages/tanstackstart-react/test/vite/sentryTanstackStart.test.ts +++ b/packages/tanstackstart-react/test/vite/sentryTanstackStart.test.ts @@ -2,6 +2,7 @@ import type { Plugin } from 'vite'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { makeAutoInstrumentMiddlewarePlugin } from '../../src/vite/autoInstrumentMiddleware'; import { sentryTanstackStart } from '../../src/vite/sentryTanstackStart'; +import { makeTunnelRoutePlugin } from '../../src/vite/tunnelRoute'; const mockSourceMapsConfigPlugin: Plugin = { name: 'sentry-tanstackstart-files-to-delete-after-upload-plugin', @@ -28,6 +29,12 @@ const mockMiddlewarePlugin: Plugin = { transform: vi.fn(), }; +const mockTunnelRoutePlugin: Plugin = { + name: 'sentry-tanstackstart-tunnel-route', + enforce: 'pre', + transform: vi.fn(), +}; + vi.mock('../../src/vite/sourceMaps', () => ({ makeAddSentryVitePlugin: vi.fn(() => [mockSourceMapsConfigPlugin, mockSentryVitePlugin]), makeEnableSourceMapsVitePlugin: vi.fn(() => [mockEnableSourceMapsPlugin]), @@ -37,6 +44,10 @@ vi.mock('../../src/vite/autoInstrumentMiddleware', () => ({ makeAutoInstrumentMiddlewarePlugin: vi.fn(() => mockMiddlewarePlugin), })); +vi.mock('../../src/vite/tunnelRoute', () => ({ + makeTunnelRoutePlugin: vi.fn(() => mockTunnelRoutePlugin), +})); + describe('sentryTanstackStart()', () => { beforeEach(() => { vi.clearAllMocks(); @@ -54,7 +65,7 @@ describe('sentryTanstackStart()', () => { expect(plugins).toEqual([mockSourceMapsConfigPlugin, mockSentryVitePlugin, mockEnableSourceMapsPlugin]); }); - it('returns no plugins in development mode', () => { + it('returns no plugins in development mode when tunnelRoute is not configured', () => { process.env.NODE_ENV = 'development'; const plugins = sentryTanstackStart({ autoInstrumentMiddleware: false }); @@ -62,6 +73,17 @@ describe('sentryTanstackStart()', () => { expect(plugins).toEqual([]); }); + it('returns only the tunnel route plugin in development mode when tunnelRoute is configured', () => { + process.env.NODE_ENV = 'development'; + + const plugins = sentryTanstackStart({ + autoInstrumentMiddleware: false, + tunnelRoute: { allowedDsns: ['https://public@o0.ingest.sentry.io/0'] }, + }); + + expect(plugins).toEqual([mockTunnelRoutePlugin]); + }); + it('returns Sentry Vite plugins but not enable source maps plugin when sourcemaps.disable is true', () => { const plugins = sentryTanstackStart({ autoInstrumentMiddleware: false, @@ -127,4 +149,29 @@ describe('sentryTanstackStart()', () => { expect(makeAutoInstrumentMiddlewarePlugin).toHaveBeenCalledWith({ enabled: true, debug: undefined }); }); }); + + describe('managed tunnel route', () => { + it('includes the managed tunnel route plugin in production when configured', () => { + const plugins = sentryTanstackStart({ + tunnelRoute: { + allowedDsns: ['https://public@o0.ingest.sentry.io/0'], + tunnel: '/monitor', + }, + sourcemaps: { disable: true }, + }); + + expect(plugins).toEqual([mockSourceMapsConfigPlugin, mockSentryVitePlugin, mockTunnelRoutePlugin, mockMiddlewarePlugin]); + }); + + it('passes tunnelRoute options through to the tunnel route plugin', () => { + const tunnelRoute = { + allowedDsns: ['https://public@o0.ingest.sentry.io/0'], + tunnel: '/monitor' as const, + }; + + sentryTanstackStart({ tunnelRoute, sourcemaps: { disable: true } }); + + expect(makeTunnelRoutePlugin).toHaveBeenCalledWith(tunnelRoute, undefined); + }); + }); }); diff --git a/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts b/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts new file mode 100644 index 000000000000..aca2e207213c --- /dev/null +++ b/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts @@ -0,0 +1,117 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { injectManagedTunnelRoute, makeTunnelRoutePlugin, resolveTunnelRoute } from '../../src/vite/tunnelRoute'; + +const MANAGED_TUNNEL_ROUTE_PATH_ENV_KEY = '__SENTRY_TANSTACKSTART_TUNNEL_ROUTE__'; + +const ROUTE_TREE_SOURCE = `import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) +`; + +const UNTYPED_ROUTE_TREE_SOURCE = ROUTE_TREE_SOURCE.replace( + 'const rootRouteChildren: RootRouteChildren = {', + 'const rootRouteChildren = {', +); + +describe('tunnelRoute vite plugin', () => { + beforeEach(() => { + delete process.env[MANAGED_TUNNEL_ROUTE_PATH_ENV_KEY]; + }); + + afterEach(() => { + delete process.env[MANAGED_TUNNEL_ROUTE_PATH_ENV_KEY]; + }); + + it('reuses the same generated tunnel route within one process', () => { + const firstTunnelRoute = resolveTunnelRoute(true); + const secondTunnelRoute = resolveTunnelRoute(true); + + expect(firstTunnelRoute).toBe(secondTunnelRoute); + expect(firstTunnelRoute).toMatch(/^\/[a-z0-9]{8}$/); + }); + + it('returns the provided static tunnel route without reusing a generated one', () => { + resolveTunnelRoute(true); + + expect(resolveTunnelRoute('/monitor')).toBe('/monitor'); + }); + + it('rejects empty allowedDsns', () => { + expect(() => makeTunnelRoutePlugin({ allowedDsns: [] })).toThrow( + '`sentryTanstackStart({ tunnelRoute })` requires at least one allowed DSN', + ); + }); + + it('rejects invalid static tunnel routes', () => { + expect(() => makeTunnelRoutePlugin({ allowedDsns: ['https://public@o0.ingest.sentry.io/0'], tunnel: 'monitor' })).toThrow( + '`tunnelRoute.tunnel` must be `true` or an absolute route path', + ); + }); + + it('injects the managed tunnel route into the generated TanStack route tree', () => { + const transformedRouteTree = injectManagedTunnelRoute(ROUTE_TREE_SOURCE, '/monitor'); + + expect(transformedRouteTree).toContain( + "import { Route as SentryManagedTunnelRouteImport } from 'virtual:sentry-tanstackstart-react/tunnel-route'", + ); + expect(transformedRouteTree).toContain('const SentryManagedTunnelRoute = SentryManagedTunnelRouteImport.update({'); + expect(transformedRouteTree).toContain('id: "/monitor"'); + expect(transformedRouteTree).toContain('path: "/monitor"'); + expect(transformedRouteTree).toContain('SentryManagedTunnelRoute: SentryManagedTunnelRoute,'); + expect(transformedRouteTree).toContain('IndexRoute: IndexRoute,'); + }); + + it('injects the managed tunnel route when rootRouteChildren is untyped', () => { + const transformedRouteTree = injectManagedTunnelRoute(UNTYPED_ROUTE_TREE_SOURCE, '/monitor'); + + expect(transformedRouteTree).toContain('const rootRouteChildren = {'); + expect(transformedRouteTree).toContain('SentryManagedTunnelRoute: SentryManagedTunnelRoute,'); + }); + + it('fails when the managed tunnel route conflicts with an existing route', () => { + expect(() => injectManagedTunnelRoute(ROUTE_TREE_SOURCE, '/')).toThrow( + 'Cannot register managed tunnel route "/" because an existing TanStack Start route already uses that path.', + ); + }); + + it('loads a virtual managed tunnel route module for a static tunnel path', async () => { + const plugin = makeTunnelRoutePlugin({ + allowedDsns: ['http://public@localhost:3031/1337'], + tunnel: '/monitor', + }); + + expect(plugin.config && plugin.config()).toEqual({ + define: { + __SENTRY_TANSTACKSTART_TUNNEL_ROUTE__: '"/monitor"', + }, + }); + + expect(plugin.resolveId && plugin.resolveId('virtual:sentry-tanstackstart-react/tunnel-route')).toBe( + '\0virtual:sentry-tanstackstart-react/tunnel-route', + ); + + const virtualRouteModule = + plugin.load && (await plugin.load('\0virtual:sentry-tanstackstart-react/tunnel-route')); + + expect(virtualRouteModule).toContain('createFileRoute("/monitor")'); + expect(virtualRouteModule).toContain('allowedDsns: ["http://public@localhost:3031/1337"]'); + }); +}); From 98202a900b7a39c32c8039a0e8b20c5a8b8151e5 Mon Sep 17 00:00:00 2001 From: Lazar Nikolov Date: Wed, 15 Apr 2026 17:48:18 -0400 Subject: [PATCH 4/7] Generate stable 8-character tunnel route paths --- packages/tanstackstart-react/src/vite/tunnelRoute.ts | 6 +++++- packages/tanstackstart-react/test/vite/tunnelRoute.test.ts | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/tanstackstart-react/src/vite/tunnelRoute.ts b/packages/tanstackstart-react/src/vite/tunnelRoute.ts index aef2b8933f44..f50a30ea7bac 100644 --- a/packages/tanstackstart-react/src/vite/tunnelRoute.ts +++ b/packages/tanstackstart-react/src/vite/tunnelRoute.ts @@ -27,7 +27,11 @@ const VIRTUAL_TUNNEL_ROUTE_ID = const RESOLVED_VIRTUAL_TUNNEL_ROUTE_ID = `\0${VIRTUAL_TUNNEL_ROUTE_ID}`; function generateRandomTunnelRoute(): string { - return `/${Math.random().toString(36).substring(2, 10)}`; + const randomPath = Array.from({ length: 8 }, () => + Math.floor(Math.random() * 36).toString(36), + ).join(""); + + return `/${randomPath}`; } export function resolveTunnelRoute(tunnel: true | string): string { diff --git a/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts b/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts index aca2e207213c..486a557e093e 100644 --- a/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts +++ b/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { vi } from 'vitest'; import { injectManagedTunnelRoute, makeTunnelRoutePlugin, resolveTunnelRoute } from '../../src/vite/tunnelRoute'; const MANAGED_TUNNEL_ROUTE_PATH_ENV_KEY = '__SENTRY_TANSTACKSTART_TUNNEL_ROUTE__'; @@ -48,6 +49,12 @@ describe('tunnelRoute vite plugin', () => { expect(firstTunnelRoute).toMatch(/^\/[a-z0-9]{8}$/); }); + it('always generates an 8-character tunnel route', () => { + vi.spyOn(Math, 'random').mockReturnValue(0.5); + + expect(resolveTunnelRoute(true)).toBe('/iiiiiiii'); + }); + it('returns the provided static tunnel route without reusing a generated one', () => { resolveTunnelRoute(true); From 320a55f40503ae539f77fcc5e54d44b94fec359b Mon Sep 17 00:00:00 2001 From: Lazar Nikolov Date: Thu, 16 Apr 2026 10:00:34 -0400 Subject: [PATCH 5/7] refactor(tanstackstart-react): mark tunnel route cache key internal --- packages/tanstackstart-react/src/vite/tunnelRoute.ts | 2 +- packages/tanstackstart-react/test/vite/tunnelRoute.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tanstackstart-react/src/vite/tunnelRoute.ts b/packages/tanstackstart-react/src/vite/tunnelRoute.ts index f50a30ea7bac..a64c98edbf6d 100644 --- a/packages/tanstackstart-react/src/vite/tunnelRoute.ts +++ b/packages/tanstackstart-react/src/vite/tunnelRoute.ts @@ -20,7 +20,7 @@ export interface TunnelRouteOptions { const MANAGED_TUNNEL_ROUTE_IMPORT = "SentryManagedTunnelRouteImport"; const MANAGED_TUNNEL_ROUTE_NAME = "SentryManagedTunnelRoute"; const MANAGED_TUNNEL_ROUTE_PATH_ENV_KEY = - "__SENTRY_TANSTACKSTART_TUNNEL_ROUTE__"; + "__SENTRY_INTERNAL_TANSTACKSTART_TUNNEL_ROUTE__"; const VIRTUAL_TUNNEL_ROUTE_ID = "virtual:sentry-tanstackstart-react/tunnel-route"; diff --git a/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts b/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts index 486a557e093e..c9411dc1745a 100644 --- a/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts +++ b/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { vi } from 'vitest'; import { injectManagedTunnelRoute, makeTunnelRoutePlugin, resolveTunnelRoute } from '../../src/vite/tunnelRoute'; -const MANAGED_TUNNEL_ROUTE_PATH_ENV_KEY = '__SENTRY_TANSTACKSTART_TUNNEL_ROUTE__'; +const MANAGED_TUNNEL_ROUTE_PATH_ENV_KEY = '__SENTRY_INTERNAL_TANSTACKSTART_TUNNEL_ROUTE__'; const ROUTE_TREE_SOURCE = `import { Route as rootRouteImport } from './routes/__root' import { Route as IndexRouteImport } from './routes/index' From 73fbbeb9cd233e04add9962b95c377aa257bc0ac Mon Sep 17 00:00:00 2001 From: Lazar Nikolov Date: Thu, 16 Apr 2026 10:18:54 -0400 Subject: [PATCH 6/7] test(tanstackstart-react): narrow tunnel response matcher --- .../tanstackstart-react/tests/tunnel.test.ts | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts index 1a585446c0f4..d5bf24e72c97 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts @@ -4,6 +4,12 @@ import { waitForError } from '@sentry-internal/test-utils'; const tunnelRouteMode = process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? (process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1' ? 'custom' : 'off'); +const expectedTunnelPathMatcher = + tunnelRouteMode === 'static' + ? '/monitor' + : tunnelRouteMode === 'custom' + ? '/custom-monitor' + : /^\/[a-z0-9]{8}$/; test.skip(tunnelRouteMode === 'off', 'Tunnel assertions only run in the tunnel-route variants'); @@ -20,7 +26,13 @@ test('Sends client-side errors through the configured tunnel route', async ({ pa const managedTunnelResponsePromise = page.waitForResponse(response => { const responseUrl = new URL(response.url()); - return responseUrl.origin === pageOrigin && response.request().method() === 'POST'; + return ( + responseUrl.origin === pageOrigin && + response.request().method() === 'POST' && + (typeof expectedTunnelPathMatcher === 'string' + ? responseUrl.pathname === expectedTunnelPathMatcher + : expectedTunnelPathMatcher.test(responseUrl.pathname)) + ); }); await page.locator('button').filter({ hasText: 'Break the client' }).click(); @@ -32,12 +44,10 @@ test('Sends client-side errors through the configured tunnel route', async ({ pa expect(managedTunnelResponse.status()).toBe(200); expect(managedTunnelUrl.origin).toBe(pageOrigin); - if (tunnelRouteMode === 'static') { - expect(managedTunnelUrl.pathname).toBe('/monitor'); - } else if (tunnelRouteMode === 'custom') { - expect(managedTunnelUrl.pathname).toBe('/custom-monitor'); + if (typeof expectedTunnelPathMatcher === 'string') { + expect(managedTunnelUrl.pathname).toBe(expectedTunnelPathMatcher); } else { - expect(managedTunnelUrl.pathname).toMatch(/^\/[a-z0-9]{8}$/); + expect(managedTunnelUrl.pathname).toMatch(expectedTunnelPathMatcher); expect(managedTunnelUrl.pathname).not.toBe('/monitor'); } From ee3da8ddad983816f983427ad2e2ef0b08b59f89 Mon Sep 17 00:00:00 2001 From: Lazar Nikolov Date: Thu, 16 Apr 2026 10:50:25 -0400 Subject: [PATCH 7/7] fix(tanstackstart-react): stub createSentryTunnelRoute on client --- .../tanstackstart-react/src/client/index.ts | 17 +++++++++++++++++ packages/tanstackstart-react/src/index.types.ts | 1 + 2 files changed, 18 insertions(+) diff --git a/packages/tanstackstart-react/src/client/index.ts b/packages/tanstackstart-react/src/client/index.ts index 3e762580830c..7607c32faaa3 100644 --- a/packages/tanstackstart-react/src/client/index.ts +++ b/packages/tanstackstart-react/src/client/index.ts @@ -2,6 +2,7 @@ // can be removed once following issue is fixed: https://github.com/import-js/eslint-plugin-import/issues/703 /* eslint-disable import/export */ import type { TanStackMiddlewareBase } from '../common/types'; +import type { CreateSentryTunnelRouteOptions } from '../server/tunnelRoute'; export * from '@sentry/react'; @@ -26,3 +27,19 @@ export const sentryGlobalRequestMiddleware: TanStackMiddlewareBase = { '~types': * The actual implementation is server-only, but this stub is needed to prevent rendering errors. */ export const sentryGlobalFunctionMiddleware: TanStackMiddlewareBase = { '~types': undefined, options: {} }; + +/** + * No-op stub for client-side builds. + * The actual implementation is server-only, but this stub is needed to prevent rendering errors. + */ +export function createSentryTunnelRoute(_options: CreateSentryTunnelRouteOptions): { + handlers: { + POST: () => Promise; + }; +} { + return { + handlers: { + POST: async () => new Response(null, { status: 500 }), + }, + }; +} diff --git a/packages/tanstackstart-react/src/index.types.ts b/packages/tanstackstart-react/src/index.types.ts index 76e4ced73c92..79edcef0bbfa 100644 --- a/packages/tanstackstart-react/src/index.types.ts +++ b/packages/tanstackstart-react/src/index.types.ts @@ -42,3 +42,4 @@ export declare const wrapMiddlewaresWithSentry: typeof serverSdk.wrapMiddlewares export declare const tanstackRouterBrowserTracingIntegration: typeof clientSdk.tanstackRouterBrowserTracingIntegration; export declare const sentryGlobalRequestMiddleware: typeof serverSdk.sentryGlobalRequestMiddleware; export declare const sentryGlobalFunctionMiddleware: typeof serverSdk.sentryGlobalFunctionMiddleware; +export declare const createSentryTunnelRoute: typeof serverSdk.createSentryTunnelRoute;