diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index b78e42dd188c..c3c8a6b96918 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -15,6 +15,7 @@ "dependencies": { "@langchain/langgraph": "^1.0.1", "@sentry/cloudflare": "10.36.0", + "@sentry/hono": "10.36.0", "hono": "^4.0.0" }, "devDependencies": { diff --git a/dev-packages/cloudflare-integration-tests/suites/hono/basic/index.ts b/dev-packages/cloudflare-integration-tests/suites/hono-integration/index.ts similarity index 89% rename from dev-packages/cloudflare-integration-tests/suites/hono/basic/index.ts rename to dev-packages/cloudflare-integration-tests/suites/hono-integration/index.ts index 6daae5f3f141..ee7d18338306 100644 --- a/dev-packages/cloudflare-integration-tests/suites/hono/basic/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/hono-integration/index.ts @@ -16,7 +16,7 @@ app.get('/json', c => { }); app.get('/error', () => { - throw new Error('Test error from Hono app'); + throw new Error('Test error from Hono app (Sentry Cloudflare SDK)'); }); app.get('/hello/:name', c => { diff --git a/dev-packages/cloudflare-integration-tests/suites/hono/basic/test.ts b/dev-packages/cloudflare-integration-tests/suites/hono-integration/test.ts similarity index 89% rename from dev-packages/cloudflare-integration-tests/suites/hono/basic/test.ts rename to dev-packages/cloudflare-integration-tests/suites/hono-integration/test.ts index 727d61cca130..8a235713681c 100644 --- a/dev-packages/cloudflare-integration-tests/suites/hono/basic/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/hono-integration/test.ts @@ -1,6 +1,6 @@ import { expect, it } from 'vitest'; -import { eventEnvelope } from '../../../expect'; -import { createRunner } from '../../../runner'; +import { eventEnvelope } from '../../expect'; +import { createRunner } from '../../runner'; it('Hono app captures errors', async ({ signal }) => { const runner = createRunner(__dirname) @@ -14,7 +14,7 @@ it('Hono app captures errors', async ({ signal }) => { values: [ { type: 'Error', - value: 'Test error from Hono app', + value: 'Test error from Hono app (Sentry Cloudflare SDK)', stacktrace: { frames: expect.any(Array), }, diff --git a/dev-packages/cloudflare-integration-tests/suites/hono/basic/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/hono-integration/wrangler.jsonc similarity index 100% rename from dev-packages/cloudflare-integration-tests/suites/hono/basic/wrangler.jsonc rename to dev-packages/cloudflare-integration-tests/suites/hono-integration/wrangler.jsonc diff --git a/dev-packages/cloudflare-integration-tests/suites/hono-sdk/index.ts b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/index.ts new file mode 100644 index 000000000000..f78fa85f7400 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/index.ts @@ -0,0 +1,40 @@ +import { sentry } from '@sentry/hono/cloudflare'; +import { Hono } from 'hono'; + +interface Env { + SENTRY_DSN: string; +} + +const app = new Hono<{ Bindings: Env }>(); + +app.use( + '*', + sentry(app, { + dsn: process.env.SENTRY_DSN, + tracesSampleRate: 1.0, + debug: true, + // todo - what is going on with this + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + integrations: integrations => integrations.filter(integration => integration.name !== 'Hono'), + }), +); + +app.get('/', c => { + return c.text('Hello from Hono on Cloudflare!'); +}); + +app.get('/json', c => { + return c.json({ message: 'Hello from Hono', framework: 'hono', platform: 'cloudflare' }); +}); + +app.get('/error', () => { + throw new Error('Test error from Hono app'); +}); + +app.get('/hello/:name', c => { + const name = c.req.param('name'); + return c.text(`Hello, ${name}!`); +}); + +export default app; diff --git a/dev-packages/cloudflare-integration-tests/suites/hono-sdk/test.ts b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/test.ts new file mode 100644 index 000000000000..c87560ab1b0a --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/test.ts @@ -0,0 +1,109 @@ +import { SDK_VERSION } from '@sentry/core'; +import { expect, it } from 'vitest'; +import { SHORT_UUID_MATCHER, UUID_MATCHER } from '../../expect'; +import { createRunner } from '../../runner'; + +it('Hono app captures errors (Hono SDK)', async ({ signal }) => { + const runner = createRunner(__dirname) + .expect(envelope => { + const [, envelopeItems] = envelope; + const [itemHeader, itemPayload] = envelopeItems[0]; + + expect(itemHeader.type).toBe('event'); + + // todo: check with function eventEnvelope + + // Validate error event structure + expect(itemPayload).toMatchObject({ + level: 'error', + platform: 'javascript', + transaction: 'GET /error', + // fixme: should be hono + sdk: { name: 'sentry.javascript.cloudflare', version: SDK_VERSION }, + // fixme: should contain trace + // trace: expect.objectContaining({ trace_id: UUID_MATCHER }), + exception: { + values: expect.arrayContaining([ + expect.objectContaining({ + type: 'Error', + value: 'Test error from Hono app', + mechanism: expect.objectContaining({ + type: 'generic', // fixme: should be 'auto.faas.hono.error_handler' + handled: true, // fixme: should be false + }), + }), + ]), + }, + request: expect.objectContaining({ + method: 'GET', + url: expect.stringContaining('/error'), + }), + }); + }) + .expect(envelope => { + const [, envelopeItems] = envelope; + const [itemHeader, itemPayload] = envelopeItems[0]; + + expect(itemHeader.type).toBe('transaction'); + + expect(itemPayload).toMatchObject({ + type: 'transaction', + platform: 'javascript', + transaction: 'GET /error', + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + op: 'http.server', + status: 'internal_error', + origin: 'auto.http.cloudflare', + }, + }, + request: expect.objectContaining({ + method: 'GET', + url: expect.stringContaining('/error'), + }), + }); + }) + + .unordered() + .start(signal); + + await runner.makeRequest('get', '/error', { expectError: true }); + await runner.completed(); +}); + +it('Hono app captures parametrized names', async ({ signal }) => { + const runner = createRunner(__dirname) + .expect(envelope => { + const [, envelopeItems] = envelope; + const [itemHeader, itemPayload] = envelopeItems[0]; + + expect(itemHeader.type).toBe('transaction'); + + expect(itemPayload).toMatchObject({ + type: 'transaction', + platform: 'javascript', + transaction: 'GET /hello/:name', + contexts: { + trace: { + span_id: SHORT_UUID_MATCHER, + trace_id: UUID_MATCHER, + op: 'http.server', + status: 'ok', + origin: 'auto.http.cloudflare', + }, + }, + request: expect.objectContaining({ + method: 'GET', + url: expect.stringContaining('/hello/:name'), + }), + }); + }) + + .unordered() + .start(signal); + + await runner.makeRequest('get', '/hello/:name', { expectError: false }); + await runner.completed(); +}); diff --git a/dev-packages/cloudflare-integration-tests/suites/hono-sdk/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/wrangler.jsonc new file mode 100644 index 000000000000..0e4895ca598f --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/wrangler.jsonc @@ -0,0 +1,7 @@ +{ + "name": "hono-sdk-worker", + "compatibility_date": "2025-06-17", + "main": "index.ts", + "compatibility_flags": ["nodejs_compat"] +} + diff --git a/dev-packages/cloudflare-integration-tests/tsconfig.json b/dev-packages/cloudflare-integration-tests/tsconfig.json index b93dc5f57c50..7d0d293b0651 100644 --- a/dev-packages/cloudflare-integration-tests/tsconfig.json +++ b/dev-packages/cloudflare-integration-tests/tsconfig.json @@ -8,6 +8,7 @@ // global fetch available in tests in lower Node versions. "lib": ["ES2020"], "esModuleInterop": true, - "types": ["@cloudflare/workers-types"] + "types": ["@cloudflare/workers-types"], + "moduleResolution": "bundler" } } diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml index 0773603b033e..6e57ee2ea812 100644 --- a/dev-packages/e2e-tests/verdaccio-config/config.yaml +++ b/dev-packages/e2e-tests/verdaccio-config/config.yaml @@ -86,6 +86,12 @@ packages: unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/hono': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/nestjs': access: $all publish: $all diff --git a/package.json b/package.json index c92a18b0dfe1..85860d4fd208 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "packages/feedback", "packages/gatsby", "packages/google-cloud-serverless", + "packages/hono", "packages/integration-shims", "packages/nestjs", "packages/nextjs", diff --git a/packages/hono/.eslintrc.js b/packages/hono/.eslintrc.js new file mode 100644 index 000000000000..6da218bd8641 --- /dev/null +++ b/packages/hono/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + env: { + node: true, + }, + extends: ['../../.eslintrc.js'], + rules: { + '@sentry-internal/sdk/no-class-field-initializers': 'off', + }, +}; diff --git a/packages/hono/LICENSE b/packages/hono/LICENSE new file mode 100644 index 000000000000..0da96cd2f885 --- /dev/null +++ b/packages/hono/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/hono/README.md b/packages/hono/README.md new file mode 100644 index 000000000000..418489148e97 --- /dev/null +++ b/packages/hono/README.md @@ -0,0 +1,43 @@ +
+
+
+
+