diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 544bb7900008..d686f8293158 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -930,7 +930,9 @@ jobs: with: node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json' - name: Set up Bun - if: contains(fromJSON('["node-exports-test-app","nextjs-16-bun", "elysia-bun"]'), matrix.test-application) + if: + contains(fromJSON('["node-exports-test-app","nextjs-16-bun", "elysia-bun", "hono-4"]'), + matrix.test-application) uses: oven-sh/setup-bun@v2 - name: Set up AWS SAM if: matrix.test-application == 'aws-serverless' diff --git a/dev-packages/e2e-tests/test-applications/hono-4/.gitignore b/dev-packages/e2e-tests/test-applications/hono-4/.gitignore new file mode 100644 index 000000000000..534f51704346 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hono-4/.gitignore @@ -0,0 +1,36 @@ +# prod +dist/ + +# dev +.yarn/ +!.yarn/releases +.vscode/* +!.vscode/launch.json +!.vscode/*.code-snippets +.idea/workspace.xml +.idea/usage.statistics.xml +.idea/shelf + +# deps +node_modules/ +.wrangler + +# env +.env +.env.production +.dev.vars + +# logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# test +test-results + +# misc +.DS_Store diff --git a/dev-packages/e2e-tests/test-applications/hono-4/.npmrc b/dev-packages/e2e-tests/test-applications/hono-4/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hono-4/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/hono-4/package.json b/dev-packages/e2e-tests/test-applications/hono-4/package.json new file mode 100644 index 000000000000..117555f7241f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hono-4/package.json @@ -0,0 +1,43 @@ +{ + "name": "hono-4", + "type": "module", + "version": "0.0.0", + "private": true, + "scripts": { + "dev:cf": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')", + "dev:node": "node --import tsx/esm --import @sentry/node/preload src/entry.node.ts", + "dev:bun": "bun src/entry.bun.ts", + "build": "wrangler deploy --dry-run", + "test:build": "pnpm install && pnpm build", + "test:assert": "TEST_ENV=production playwright test" + }, + "dependencies": { + "@sentry/hono": "latest || *", + "@sentry/node": "latest || *", + "@hono/node-server": "^1.19.10", + "hono": "^4.12.14" + }, + "devDependencies": { + "@playwright/test": "~1.56.0", + "@cloudflare/workers-types": "^4.20240725.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "tsx": "^4.20.3", + "typescript": "^5.5.2", + "wrangler": "^4.61.0" + }, + "volta": { + "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "assert-command": "RUNTIME=node pnpm test:assert", + "label": "hono-4 (node)" + }, + { + "assert-command": "RUNTIME=bun pnpm test:assert", + "label": "hono-4 (bun)" + } + ] + } +} diff --git a/dev-packages/e2e-tests/test-applications/hono-4/playwright.config.ts b/dev-packages/e2e-tests/test-applications/hono-4/playwright.config.ts new file mode 100644 index 000000000000..74a21e10a349 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hono-4/playwright.config.ts @@ -0,0 +1,32 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +type Runtime = 'cloudflare' | 'node' | 'bun'; + +const RUNTIME = (process.env.RUNTIME || 'cloudflare') as Runtime; + +const testEnv = process.env.TEST_ENV; + +if (!testEnv) { + throw new Error('No test env defined'); +} + +const APP_PORT = 38787; + +const startCommands: Record = { + cloudflare: `pnpm dev:cf --port ${APP_PORT}`, + node: `pnpm dev:node`, + bun: `pnpm dev:bun`, +}; + +const config = getPlaywrightConfig( + { + startCommand: startCommands[RUNTIME], + port: APP_PORT, + }, + { + workers: '100%', + retries: 0, + }, +); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.bun.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.bun.ts new file mode 100644 index 000000000000..2a27d1adb8a9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.bun.ts @@ -0,0 +1,24 @@ +import { Hono } from 'hono'; +import { sentry } from '@sentry/hono/bun'; +import { addRoutes } from './routes'; + +const app = new Hono<{ Bindings: { E2E_TEST_DSN: string } }>(); + +app.use( + // @ts-expect-error - Env is not yet in type + sentry(app, { + dsn: process.env.E2E_TEST_DSN, + environment: 'qa', + tracesSampleRate: 1.0, + tunnel: 'http://localhost:3031/', + }), +); + +addRoutes(app); + +const port = Number(process.env.PORT || 38787); + +export default { + port, + fetch: app.fetch, +}; diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.cloudflare.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.cloudflare.ts new file mode 100644 index 000000000000..e348dde56226 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.cloudflare.ts @@ -0,0 +1,18 @@ +import { Hono } from 'hono'; +import { sentry } from '@sentry/hono/cloudflare'; +import { addRoutes } from './routes'; + +const app = new Hono<{ Bindings: { E2E_TEST_DSN: string } }>(); + +app.use( + sentry(app, env => ({ + dsn: env.E2E_TEST_DSN, + environment: 'qa', + tracesSampleRate: 1.0, + tunnel: 'http://localhost:3031/', + })), +); + +addRoutes(app); + +export default app; diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/entry.node.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.node.ts new file mode 100644 index 000000000000..eb2c669c6806 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hono-4/src/entry.node.ts @@ -0,0 +1,24 @@ +import { Hono } from 'hono'; +import { sentry } from '@sentry/hono/node'; +import { serve } from '@hono/node-server'; +import { addRoutes } from './routes'; + +const app = new Hono<{ Bindings: { E2E_TEST_DSN: string } }>(); + +app.use( + // @ts-expect-error - Env is not yet in type + sentry(app, { + dsn: process.env.E2E_TEST_DSN, + environment: 'qa', + tracesSampleRate: 1.0, + tunnel: 'http://localhost:3031/', + }), +); + +addRoutes(app); + +const port = Number(process.env.PORT || 38787); + +serve({ fetch: app.fetch, port }, () => { + console.log(`Hono (Node) listening on port ${port}`); +}); diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts new file mode 100644 index 000000000000..fbb273c7c425 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts @@ -0,0 +1,24 @@ +import type { Hono } from 'hono'; +import { HTTPException } from 'hono/http-exception'; + +export function addRoutes(app: Hono<{ Bindings: { E2E_TEST_DSN: string } }>): void { + app.get('/', c => { + return c.text('Hello Hono!'); + }); + + app.get('/test-param/:paramId', c => { + return c.json({ paramId: c.req.param('paramId') }); + }); + + app.get('/error/:cause', c => { + throw new Error('This is a test error for Sentry!', { + cause: c.req.param('cause'), + }); + }); + + app.get('/http-exception/:code', c => { + // oxlint-disable-next-line typescript/no-explicit-any + const code = Number(c.req.param('code')) as any; + throw new HTTPException(code, { message: `HTTPException ${code}` }); + }); +} diff --git a/dev-packages/e2e-tests/test-applications/hono-4/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/hono-4/start-event-proxy.mjs new file mode 100644 index 000000000000..cd6f91b3455d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hono-4/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'hono-4', +}); diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts new file mode 100644 index 000000000000..e85958e8328b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts @@ -0,0 +1,54 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +const APP_NAME = 'hono-4'; + +test('captures error thrown in route handler', async ({ baseURL }) => { + const errorWaiter = waitForError(APP_NAME, event => { + return event.exception?.values?.[0]?.value === 'This is a test error for Sentry!'; + }); + + const response = await fetch(`${baseURL}/error/test-cause`); + expect(response.status).toBe(500); + + const event = await errorWaiter; + expect(event.exception?.values?.[0]?.value).toBe('This is a test error for Sentry!'); +}); + +test('captures HTTPException with 502 status', async ({ baseURL }) => { + const errorWaiter = waitForError(APP_NAME, event => { + return event.exception?.values?.[0]?.value === 'HTTPException 502'; + }); + + const response = await fetch(`${baseURL}/http-exception/502`); + expect(response.status).toBe(502); + + const event = await errorWaiter; + expect(event.exception?.values?.[0]?.value).toBe('HTTPException 502'); +}); + +// TODO: 401 and 404 HTTPExceptions should not be captured by Sentry by default, +// but currently they are. Fix the filtering and update these tests accordingly. +test('captures HTTPException with 401 status', async ({ baseURL }) => { + const errorWaiter = waitForError(APP_NAME, event => { + return event.exception?.values?.[0]?.value === 'HTTPException 401'; + }); + + const response = await fetch(`${baseURL}/http-exception/401`); + expect(response.status).toBe(401); + + const event = await errorWaiter; + expect(event.exception?.values?.[0]?.value).toBe('HTTPException 401'); +}); + +test('captures HTTPException with 404 status', async ({ baseURL }) => { + const errorWaiter = waitForError(APP_NAME, event => { + return event.exception?.values?.[0]?.value === 'HTTPException 404'; + }); + + const response = await fetch(`${baseURL}/http-exception/404`); + expect(response.status).toBe(404); + + const event = await errorWaiter; + expect(event.exception?.values?.[0]?.value).toBe('HTTPException 404'); +}); diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/tracing.test.ts new file mode 100644 index 000000000000..58c73c6a8369 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/tracing.test.ts @@ -0,0 +1,41 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +const APP_NAME = 'hono-4'; + +test('sends a transaction for the index route', async ({ baseURL }) => { + const transactionWaiter = waitForTransaction(APP_NAME, event => { + return event.transaction === 'GET /'; + }); + + const response = await fetch(`${baseURL}/`); + expect(response.status).toBe(200); + + const transaction = await transactionWaiter; + expect(transaction.contexts?.trace?.op).toBe('http.server'); +}); + +test('sends a transaction for a parameterized route', async ({ baseURL }) => { + const transactionWaiter = waitForTransaction(APP_NAME, event => { + return event.transaction === 'GET /test-param/:paramId'; + }); + + const response = await fetch(`${baseURL}/test-param/123`); + expect(response.status).toBe(200); + + const transaction = await transactionWaiter; + expect(transaction.contexts?.trace?.op).toBe('http.server'); + expect(transaction.transaction).toBe('GET /test-param/:paramId'); +}); + +test('sends a transaction for a route that throws', async ({ baseURL }) => { + const transactionWaiter = waitForTransaction(APP_NAME, event => { + return event.transaction === 'GET /error/:cause'; + }); + + await fetch(`${baseURL}/error/test-cause`); + + const transaction = await transactionWaiter; + expect(transaction.contexts?.trace?.op).toBe('http.server'); + expect(transaction.contexts?.trace?.status).toBe('internal_error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tsconfig.json b/dev-packages/e2e-tests/test-applications/hono-4/tsconfig.json new file mode 100644 index 000000000000..3c4abeff44d6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hono-4/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "lib": ["ESNext"], + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx", + "types": ["@cloudflare/workers-types"] + } +} diff --git a/dev-packages/e2e-tests/test-applications/hono-4/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/hono-4/wrangler.jsonc new file mode 100644 index 000000000000..d4344dfa198a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hono-4/wrangler.jsonc @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "hono-4", + "main": "src/entry.cloudflare.ts", + "compatibility_date": "2026-04-20", + "compatibility_flags": ["nodejs_compat"], +}