diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json
index 7716b2777cb7..5e33d56454f0 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.33.0",
+ "@sentry/hono": "10.33.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..7b2e6b672425
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/index.ts
@@ -0,0 +1,40 @@
+import { sentry } from '@sentry/hono/cloudflare-workers';
+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/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 @@
+
+
+
+
+
+
+# Official Sentry SDK for Hono (ALPHA)
+
+[](https://www.npmjs.com/package/@sentry/hono)
+[](https://www.npmjs.com/package/@sentry/hono)
+[](https://www.npmjs.com/package/@sentry/hono)
+
+## Links
+
+- [Official SDK Docs](https://docs.sentry.io/quickstart/)
+
+## Install
+
+To get started, first install the `@sentry/hono` package:
+
+```bash
+npm install @sentry/hono
+```
+
+## Setup (Cloudflare Workers)
+
+### Enable Node.js compatibility
+
+Either set the `nodejs_als` or `nodejs_compat` compatibility flags in your `wrangler.jsonc`/`wrangler.toml` config. This is because the SDK needs access to the `AsyncLocalStorage` API to work correctly.
+
+```jsonc {tabTitle:JSON} {filename:wrangler.jsonc}
+{
+ "compatibility_flags": [
+ "nodejs_als",
+ // "nodejs_compat"
+ ],
+}
+```
+
+```toml {tabTitle:Toml} {filename:wrangler.toml}
+compatibility_flags = ["nodejs_als"]
+# compatibility_flags = ["nodejs_compat"]
+```
diff --git a/packages/hono/package.json b/packages/hono/package.json
new file mode 100644
index 000000000000..4568bf2d8449
--- /dev/null
+++ b/packages/hono/package.json
@@ -0,0 +1,100 @@
+{
+ "name": "@sentry/hono",
+ "version": "10.33.0",
+ "description": "Official Sentry SDK for Hono (ALPHA)",
+ "repository": "git://github.com/getsentry/sentry-javascript.git",
+ "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/hono",
+ "author": "Sentry",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "files": [
+ "/build"
+ ],
+ "main": "build/cjs/index.js",
+ "module": "build/esm/index.js",
+ "types": "build/types/index.d.ts",
+ "exports": {
+ "./package.json": "./package.json",
+ ".": {
+ "import": {
+ "types": "./build/types/index.d.ts",
+ "default": "./build/esm/index.js"
+ },
+ "require": {
+ "types": "./build/types/index.d.ts",
+ "default": "./build/cjs/index.js"
+ }
+ },
+ "./cloudflare-workers": {
+ "import": {
+ "types": "./build/types/index.cloudflare.d.ts",
+ "default": "./build/esm/index.cloudflare.js"
+ },
+ "require": {
+ "types": "./build/types/index.cloudflare.d.ts",
+ "default": "./build/cjs/index.cloudflare.js"
+ }
+ }
+ },
+ "typesVersions": {
+ "<5.0": {
+ "build/types/index.d.ts": [
+ "build/types-ts3.8/index.d.ts"
+ ],
+ "build/types/index.cloudflare.d.ts": [
+ "build/types-ts3.8/index.cloudflare.d.ts"
+ ]
+ }
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "dependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@sentry/cloudflare": "10.33.0",
+ "@sentry/core": "10.33.0",
+ "@sentry/node": "10.33.0"
+ },
+ "peerDependencies": {
+ "@cloudflare/workers-types": "^4.x",
+ "hono": "^4.10.4"
+ },
+ "peerDependenciesMeta": {
+ "@cloudflare/workers-types": {
+ "optional": true
+ }
+ },
+ "devDependencies": {
+ "@cloudflare/workers-types": "4.20250922.0",
+ "@types/node": "^18.19.1",
+ "wrangler": "4.22.0"
+ },
+ "scripts": {
+ "build": "run-p build:transpile build:types",
+ "build:dev": "yarn build",
+ "build:transpile": "rollup -c rollup.npm.config.mjs",
+ "build:types": "run-s build:types:core build:types:downlevel",
+ "build:types:core": "tsc -p tsconfig.types.json",
+ "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8",
+ "build:watch": "run-p build:transpile:watch build:types:watch",
+ "build:dev:watch": "yarn build:watch",
+ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch",
+ "build:types:watch": "tsc -p tsconfig.types.json --watch",
+ "build:tarball": "npm pack",
+ "circularDepCheck": "madge --circular src/index.ts",
+ "clean": "rimraf build coverage sentry-hono-*.tgz",
+ "fix": "eslint . --format stylish --fix",
+ "lint": "eslint . --format stylish",
+ "lint:es-compatibility": "es-check es2022 ./build/cjs/*.js && es-check es2022 ./build/esm/*.js --module",
+ "test": "yarn test:unit",
+ "test:unit": "vitest run",
+ "test:watch": "vitest --watch",
+ "yalc:publish": "yalc publish --push --sig"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ },
+ "sideEffects": false
+}
diff --git a/packages/hono/rollup.npm.config.mjs b/packages/hono/rollup.npm.config.mjs
new file mode 100644
index 000000000000..6f491584a9d0
--- /dev/null
+++ b/packages/hono/rollup.npm.config.mjs
@@ -0,0 +1,21 @@
+import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
+
+const baseConfig = makeBaseNPMConfig({
+ entrypoints: ['src/index.ts', 'src/index.cloudflare.ts'],
+ packageSpecificConfig: {
+ output: {
+ preserveModulesRoot: 'src',
+ },
+ },
+});
+
+const defaultExternal = baseConfig.external;
+baseConfig.external = id => {
+ if (defaultExternal.includes(id)) {
+ return true;
+ }
+ // Mark all hono subpaths as external
+ return !!(id === 'hono' || id.startsWith('hono/'));
+};
+
+export default [...makeNPMConfigVariants(baseConfig)];
diff --git a/packages/hono/src/cloudflare/middleware.ts b/packages/hono/src/cloudflare/middleware.ts
new file mode 100644
index 000000000000..c9ccb845359b
--- /dev/null
+++ b/packages/hono/src/cloudflare/middleware.ts
@@ -0,0 +1,24 @@
+import { withSentry } from '@sentry/cloudflare';
+import { type BaseTransportOptions, debug, type Options } from '@sentry/core';
+import type { Context, Hono, MiddlewareHandler } from 'hono';
+import { requestHandler, responseHandler } from '../shared/middlewareHandlers';
+
+export interface HonoOptions extends Options {
+ context?: Context;
+}
+
+export const sentry = (app: Hono, options: HonoOptions | undefined = {}): MiddlewareHandler => {
+ const isDebug = options.debug;
+
+ isDebug && debug.log('Initialized Sentry Hono middleware (Cloudflare)');
+
+ withSentry(() => options, app);
+
+ return async (context, next) => {
+ requestHandler(context);
+
+ await next(); // Handler runs in between Request above ⤴ and Response below ⤵
+
+ responseHandler(context);
+ };
+};
diff --git a/packages/hono/src/index.cloudflare.ts b/packages/hono/src/index.cloudflare.ts
new file mode 100644
index 000000000000..cba517e1d295
--- /dev/null
+++ b/packages/hono/src/index.cloudflare.ts
@@ -0,0 +1 @@
+export { sentry } from './cloudflare/middleware';
diff --git a/packages/hono/src/index.ts b/packages/hono/src/index.ts
new file mode 100644
index 000000000000..bafed89f04bd
--- /dev/null
+++ b/packages/hono/src/index.ts
@@ -0,0 +1,2 @@
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+export const sentryNoOp = () => {};
diff --git a/packages/hono/src/index.types.ts b/packages/hono/src/index.types.ts
new file mode 100644
index 000000000000..cb0ff5c3b541
--- /dev/null
+++ b/packages/hono/src/index.types.ts
@@ -0,0 +1 @@
+export {};
diff --git a/packages/hono/src/shared/middlewareHandlers.ts b/packages/hono/src/shared/middlewareHandlers.ts
new file mode 100644
index 000000000000..01cea49c5548
--- /dev/null
+++ b/packages/hono/src/shared/middlewareHandlers.ts
@@ -0,0 +1,43 @@
+import { getIsolationScope } from '@sentry/cloudflare';
+import {
+ getActiveSpan,
+ getClient,
+ getDefaultIsolationScope,
+ getRootSpan,
+ updateSpanName,
+ winterCGRequestToRequestData,
+} from '@sentry/core';
+import type { Context } from 'hono';
+import { routePath } from 'hono/route';
+import { hasFetchEvent } from '../utils/hono-context';
+
+/**
+ * todo
+ */
+export function requestHandler(context: Context): void {
+ const defaultScope = getDefaultIsolationScope();
+ const currentIsolationScope = getIsolationScope();
+
+ const isolationScope = defaultScope === currentIsolationScope ? defaultScope : currentIsolationScope;
+
+ isolationScope.setSDKProcessingMetadata({
+ normalizedRequest: winterCGRequestToRequestData(hasFetchEvent(context) ? context.event.request : context.req.raw),
+ });
+}
+
+/**
+ * todo
+ */
+export function responseHandler(context: Context): void {
+ const activeSpan = getActiveSpan();
+ if (activeSpan) {
+ activeSpan.updateName(`${context.req.method} ${routePath(context)}`);
+ updateSpanName(getRootSpan(activeSpan), `${context.req.method} ${routePath(context)}`);
+ }
+
+ getIsolationScope().setTransactionName(`${context.req.method} ${routePath(context)}`);
+
+ if (context.error) {
+ getClient()?.captureException(context.error);
+ }
+}
diff --git a/packages/hono/src/utils/hono-context.ts b/packages/hono/src/utils/hono-context.ts
new file mode 100644
index 000000000000..96df44ee655a
--- /dev/null
+++ b/packages/hono/src/utils/hono-context.ts
@@ -0,0 +1,15 @@
+import type { Context } from 'hono';
+
+/**
+ * Checks whether the given Hono context has a fetch event.
+ */
+export function hasFetchEvent(c: Context): boolean {
+ let hasFetchEvent = true;
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
+ c.event;
+ } catch {
+ hasFetchEvent = false;
+ }
+ return hasFetchEvent;
+}
diff --git a/packages/hono/tsconfig.json b/packages/hono/tsconfig.json
new file mode 100644
index 000000000000..ff89f0feaa23
--- /dev/null
+++ b/packages/hono/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+
+ "include": ["src/**/*"],
+
+ "compilerOptions": {
+ "module": "esnext",
+ "types": ["node", "@cloudflare/workers-types"]
+ }
+}
diff --git a/packages/hono/tsconfig.test.json b/packages/hono/tsconfig.test.json
new file mode 100644
index 000000000000..00cada2d8bcf
--- /dev/null
+++ b/packages/hono/tsconfig.test.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+
+ "include": ["test/**/*", "vite.config.ts"],
+
+ "compilerOptions": {
+ // other package-specific, test-specific options
+ }
+}
diff --git a/packages/hono/tsconfig.types.json b/packages/hono/tsconfig.types.json
new file mode 100644
index 000000000000..65455f66bd75
--- /dev/null
+++ b/packages/hono/tsconfig.types.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": true,
+ "emitDeclarationOnly": true,
+ "outDir": "build/types"
+ }
+}
diff --git a/packages/hono/vite.config.ts b/packages/hono/vite.config.ts
new file mode 100644
index 000000000000..b2150cd225a4
--- /dev/null
+++ b/packages/hono/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vitest/config';
+import baseConfig from '../../vite/vite.config';
+
+export default defineConfig({
+ ...baseConfig,
+});