diff --git a/dev-packages/cloudflare-integration-tests/expect.ts b/dev-packages/cloudflare-integration-tests/expect.ts index 11631832852b..b33926ffce11 100644 --- a/dev-packages/cloudflare-integration-tests/expect.ts +++ b/dev-packages/cloudflare-integration-tests/expect.ts @@ -19,13 +19,13 @@ function dropUndefinedKeys>(obj: T): T { return obj; } -function getSdk(): SdkInfo { +function getSdk(sdk: 'cloudflare' | 'hono'): SdkInfo { return { integrations: expect.any(Array), - name: 'sentry.javascript.cloudflare', + name: `sentry.javascript.${sdk}`, packages: [ { - name: 'npm:@sentry/cloudflare', + name: `npm:@sentry/${sdk}`, version: SDK_VERSION, }, ], @@ -46,24 +46,27 @@ function defaultContexts(eventContexts: Contexts = {}): Contexts { }); } -export function expectedEvent(event: Event): Event { +export function expectedEvent(event: Event, { sdk }: { sdk: 'cloudflare' | 'hono' }): Event { return dropUndefinedKeys({ event_id: UUID_MATCHER, timestamp: expect.any(Number), environment: 'production', platform: 'javascript', - sdk: getSdk(), + sdk: getSdk(sdk), ...event, contexts: defaultContexts(event.contexts), }); } -export function eventEnvelope(event: Event, includeSampleRand = false): Envelope { +export function eventEnvelope( + event: Event, + { includeSampleRand = false, sdk = 'cloudflare' }: { includeSampleRand?: boolean; sdk?: 'cloudflare' | 'hono' } = {}, +): Envelope { return [ { event_id: UUID_MATCHER, sent_at: ISO_DATE_MATCHER, - sdk: { name: 'sentry.javascript.cloudflare', version: SDK_VERSION }, + sdk: { name: `sentry.javascript.${sdk}`, version: SDK_VERSION }, trace: { environment: event.environment || 'production', public_key: 'public', @@ -74,6 +77,6 @@ export function eventEnvelope(event: Event, includeSampleRand = false): Envelope transaction: expect.any(String), }, }, - [[{ type: 'event' }, expectedEvent(event)]], + [[{ type: 'event' }, expectedEvent(event, { sdk })]], ]; } diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index fbeaa52b65d4..ee5d883ae4c1 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.38.0", + "@sentry/hono": "10.38.0", "hono": "^4.11.7" }, "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 87% 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..0cf4f1dec328 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), }, @@ -28,7 +28,7 @@ it('Hono app captures errors', async ({ signal }) => { url: expect.any(String), }, }, - true, + { includeSampleRand: true }, ), ) // Second envelope: transaction event 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..63464d4e2237 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/index.ts @@ -0,0 +1,38 @@ +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, + // fixme - check out what removing this integration changes + // 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..9c1f3cda8d66 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/test.ts @@ -0,0 +1,99 @@ +import { expect, it } from 'vitest'; +import { eventEnvelope, 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( + eventEnvelope( + { + level: 'error', + transaction: 'GET /error', + exception: { + values: [ + { + type: 'Error', + value: 'Test error from Hono app', + stacktrace: { + frames: expect.any(Array), + }, + mechanism: { type: 'auto.faas.hono.error_handler', handled: false }, + }, + ], + }, + request: { + headers: expect.any(Object), + method: 'GET', + url: expect.any(String), + }, + }, + { includeSampleRand: true, sdk: 'hono' }, + ), + ) + .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/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 1b2a32eb5388..bff2c835b563 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,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..0ecae617386e --- /dev/null +++ b/packages/hono/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 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..204123308319 --- /dev/null +++ b/packages/hono/README.md @@ -0,0 +1,58 @@ +

+ + Sentry + +

+ +# Official Sentry SDK for Hono (ALPHA) + +[![npm version](https://img.shields.io/npm/v/@sentry/hono.svg)](https://www.npmjs.com/package/@sentry/hono) +[![npm dm](https://img.shields.io/npm/dm/@sentry/hono.svg)](https://www.npmjs.com/package/@sentry/hono) +[![npm dt](https://img.shields.io/npm/dt/@sentry/hono.svg)](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_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_compat"], +} +``` + +```toml {tabTitle:Toml} {filename:wrangler.toml} +compatibility_flags = ["nodejs_compat"] +``` + +### Initialize Sentry in your Hono app + +Initialize the Sentry Hono middleware as early as possible in your app: + +```typescript +import { sentry } from '@sentry/hono/cloudflare'; + +const app = new Hono(); + +app.use( + '*', + sentry(app, { + dsn: 'your-sentry-dsn', + }), +); + +export default app; +``` diff --git a/packages/hono/package.json b/packages/hono/package.json new file mode 100644 index 000000000000..f6b42cb585c1 --- /dev/null +++ b/packages/hono/package.json @@ -0,0 +1,100 @@ +{ + "name": "@sentry/hono", + "version": "10.38.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": { + "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.38.0", + "@sentry/core": "10.38.0", + "@sentry/node": "10.38.0" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.x", + "hono": "^4.x" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + }, + "devDependencies": { + "@cloudflare/workers-types": "4.20250922.0", + "@types/node": "^18.19.1", + "wrangler": "4.62.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..43f229a9a5f1 --- /dev/null +++ b/packages/hono/src/cloudflare/middleware.ts @@ -0,0 +1,25 @@ +import { withSentry } from '@sentry/cloudflare'; +import { applySdkMetadata, 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)'); + + applySdkMetadata(options, 'hono'); + 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..cb0ff5c3b541 --- /dev/null +++ b/packages/hono/src/index.ts @@ -0,0 +1 @@ +export {}; 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..6edc58eb9939 --- /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'; + +/** + * Request handler for Hono framework + */ +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), + }); +} + +/** + * Response handler for Hono framework + */ +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/test/cloudflare/middleware.test.ts b/packages/hono/test/cloudflare/middleware.test.ts new file mode 100644 index 000000000000..dff1d154dd16 --- /dev/null +++ b/packages/hono/test/cloudflare/middleware.test.ts @@ -0,0 +1,128 @@ +import * as SentryCloudflare from '@sentry/cloudflare'; +import * as SentryCore from '@sentry/core'; +import { SDK_VERSION } from '@sentry/core'; +import { Hono } from 'hono'; +import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; +import { sentry } from '../../src/cloudflare/middleware'; + +vi.mock('@sentry/cloudflare', { spy: true }); +vi.mock('@sentry/core', async () => { + const actual = await vi.importActual('@sentry/core'); + return { + ...actual, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + applySdkMetadata: vi.fn(actual.applySdkMetadata), + }; +}); + +const withSentryMock = SentryCloudflare.withSentry as Mock; +const applySdkMetadataMock = SentryCore.applySdkMetadata as Mock; + +describe('Hono Cloudflare Middleware', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('sentry middleware', () => { + it('calls applySdkMetadata with "hono"', () => { + const app = new Hono(); + const options = { + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }; + + sentry(app, options); + + expect(applySdkMetadataMock).toHaveBeenCalledTimes(1); + expect(applySdkMetadataMock).toHaveBeenCalledWith(options, 'hono'); + }); + + it('calls withSentry with modified options', () => { + const app = new Hono(); + const options = { + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }; + + sentry(app, options); + + expect(withSentryMock).toHaveBeenCalledTimes(1); + expect(withSentryMock).toHaveBeenCalledWith(expect.any(Function), app); + + // Get the options callback and call it + const optionsCallback = withSentryMock.mock.calls[0]?.[0]; + expect(optionsCallback).toBeInstanceOf(Function); + + const result = optionsCallback(); + + // After applySdkMetadata is called, options should have _metadata.sdk + expect(result.dsn).toBe('https://public@dsn.ingest.sentry.io/1337'); + expect(result._metadata?.sdk?.name).toBe('sentry.javascript.hono'); + expect(result._metadata?.sdk?.version).toBe(SDK_VERSION); + expect(result._metadata?.sdk?.packages).toEqual([ + { + name: 'npm:@sentry/hono', + version: SDK_VERSION, + }, + ]); + }); + + it('calls applySdkMetadata before withSentry', () => { + const app = new Hono(); + const options = { + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }; + + sentry(app, options); + + // Verify applySdkMetadata was called before withSentry + const applySdkMetadataCallOrder = applySdkMetadataMock.mock.invocationCallOrder[0]; + const withSentryCallOrder = withSentryMock.mock.invocationCallOrder[0]; + + expect(applySdkMetadataCallOrder).toBeLessThan(withSentryCallOrder as number); + }); + + it('preserves all user options', () => { + const app = new Hono(); + const options = { + dsn: 'https://public@dsn.ingest.sentry.io/1337', + environment: 'production', + sampleRate: 0.5, + tracesSampleRate: 1.0, + debug: true, + }; + + sentry(app, options); + + const optionsCallback = withSentryMock.mock.calls[0]?.[0]; + const result = optionsCallback(); + + expect(result).toMatchObject({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + environment: 'production', + sampleRate: 0.5, + tracesSampleRate: 1.0, + debug: true, + }); + }); + + it('returns a middleware handler function', () => { + const app = new Hono(); + const options = { + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }; + + const middleware = sentry(app, options); + + expect(middleware).toBeDefined(); + expect(typeof middleware).toBe('function'); + expect(middleware).toHaveLength(2); // Hono middleware takes (context, next) + }); + + it('returns an async middleware handler', async () => { + const app = new Hono(); + const middleware = sentry(app, {}); + + expect(middleware.constructor.name).toBe('AsyncFunction'); + }); + }); +}); 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, +}); diff --git a/yarn.lock b/yarn.lock index f03469ec13ce..7d20e13fd609 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2747,31 +2747,61 @@ resolved "https://registry.yarnpkg.com/@cloudflare/unenv-preset/-/unenv-preset-2.11.0.tgz#cac63e7c9597176767e1830d75abfdbfdcbd42b8" integrity sha512-z3hxFajL765VniNPGV0JRStZolNz63gU3B3AktwoGdDlnQvz5nP+Ah4RL04PONlZQjwmDdGHowEStJ94+RsaJg== +"@cloudflare/unenv-preset@2.12.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@cloudflare/unenv-preset/-/unenv-preset-2.12.0.tgz#3448ce6a88a8f917a3d49c916b0bc48393f50a32" + integrity sha512-NK4vN+2Z/GbfGS4BamtbbVk1rcu5RmqaYGiyHJQrA09AoxdZPHDF3W/EhgI0YSK8p3vRo/VNCtbSJFPON7FWMQ== + "@cloudflare/workerd-darwin-64@1.20260124.0": version "1.20260124.0" resolved "https://registry.yarnpkg.com/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260124.0.tgz#958e475f8a5fce1d9453d47b98c09526f1a45438" integrity sha512-VuqscLhiiVIf7t/dcfkjtT0LKJH+a06KUFwFTHgdTcqyLbFZ44u1SLpOONu5fyva4A9MdaKh9a+Z/tBC1d76nw== +"@cloudflare/workerd-darwin-64@1.20260131.0": + version "1.20260131.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260131.0.tgz#1c295393b261ed27c85be1d99e3ce63d7f2e88e8" + integrity sha512-+1X4qErc715NUhJZNhtlpuCxajhD5YNre7Cz50WPMmj+BMUrh9h7fntKEadtrUo5SM2YONY7CDzK7wdWbJJBVA== + "@cloudflare/workerd-darwin-arm64@1.20260124.0": version "1.20260124.0" resolved "https://registry.yarnpkg.com/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260124.0.tgz#8dad514564bcadc2fb5ac449bc24837d3d1533f5" integrity sha512-PfnjoFooPgRKFUIZcEP9irnn5Y7OgXinjM+IMlKTdEyLWjMblLsbsqAgydf75+ii0715xAeUlWQjZrWdyOZjMw== +"@cloudflare/workerd-darwin-arm64@1.20260131.0": + version "1.20260131.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260131.0.tgz#fcead80d4052f4bf243970e9eaf47b85ddcafa28" + integrity sha512-M84mXR8WEMEBuX4/dL2IQ4wHV/ALwYjx9if5ePZR8rdbD7if/fkEEoMBq0bGS/1gMLRqqCZLstabxHV+g92NNg== + "@cloudflare/workerd-linux-64@1.20260124.0": version "1.20260124.0" resolved "https://registry.yarnpkg.com/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260124.0.tgz#91845c0f67c73abc2959c3ab90b474cd88e6d6ec" integrity sha512-KSkZl4kwcWeFXI7qsaLlMnKwjgdZwI0OEARjyZpiHCxJCqAqla9XxQKNDscL2Z3qUflIo30i+uteGbFrhzuVGQ== +"@cloudflare/workerd-linux-64@1.20260131.0": + version "1.20260131.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260131.0.tgz#81a590e0c306f746c37494d7211e78d9bf7fc333" + integrity sha512-SWzr48bCL9y5wjkj23tXS6t/6us99EAH9T5TAscMV0hfJFZQt97RY/gaHKyRRjFv6jfJZvk7d4g+OmGeYBnwcg== + "@cloudflare/workerd-linux-arm64@1.20260124.0": version "1.20260124.0" resolved "https://registry.yarnpkg.com/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260124.0.tgz#42baebe126d430ae1dfeb23002a456239f24305b" integrity sha512-61xjSUNk745EVV4vXZP0KGyLCatcmamfBB+dcdQ8kDr6PrNU4IJ1kuQFSJdjybyDhJRm4TpGVywq+9hREuF7xA== +"@cloudflare/workerd-linux-arm64@1.20260131.0": + version "1.20260131.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260131.0.tgz#3146ab166a14863f725fd42539f248d92970a935" + integrity sha512-mL0kLPGIBJRPeHS3+erJ2t5dJT3ODhsKvR9aA4BcsY7M30/QhlgJIF6wsgwNisTJ23q8PbobZNHBUKIe8l/E9A== + "@cloudflare/workerd-windows-64@1.20260124.0": version "1.20260124.0" resolved "https://registry.yarnpkg.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260124.0.tgz#2721810ca8bcbd2b9dbaf4fb13a971b8ea9e4c21" integrity sha512-j9O11pwQQV6Vi3peNrJoyIas3SrZHlPj0Ah+z1hDW9o1v35euVBQJw/PuzjPOXxTFUlGQoMJdfzPsO9xP86g7A== +"@cloudflare/workerd-windows-64@1.20260131.0": + version "1.20260131.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260131.0.tgz#05d877eb8a0eab80d2e8a3d06ff7d23f6365e0f3" + integrity sha512-hoQqTFBpP1zntP2OQSpt5dEWbd9vSBliK+G7LmDXjKitPkmkRFo2PB4P9aBRE1edPAIO/fpdoJv928k2HaAn4A== + "@cloudflare/workers-types@4.20250922.0", "@cloudflare/workers-types@^4.20250922.0": version "4.20250922.0" resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20250922.0.tgz#a159fbf3bb785fa85b473ecfaa8c501525827885" @@ -22410,6 +22440,18 @@ miniflare@4.20260124.0: ws "8.18.0" youch "4.1.0-beta.10" +miniflare@4.20260131.0: + version "4.20260131.0" + resolved "https://registry.yarnpkg.com/miniflare/-/miniflare-4.20260131.0.tgz#2615af3dae74e8346ca7ec9cae6c106a94cf5a2a" + integrity sha512-CtObRzlAzOUpCFH+MgImykxmDNKthrgIYtC+oLC3UGpve6bGLomKUW4u4EorTvzlQFHe66/9m/+AYbBbpzG0mQ== + dependencies: + "@cspotcode/source-map-support" "0.8.1" + sharp "^0.34.5" + undici "7.18.2" + workerd "1.20260131.0" + ws "8.18.0" + youch "4.1.0-beta.10" + minimalistic-assert@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -29076,7 +29118,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" @@ -31801,6 +31842,17 @@ workerd@1.20260124.0: "@cloudflare/workerd-linux-arm64" "1.20260124.0" "@cloudflare/workerd-windows-64" "1.20260124.0" +workerd@1.20260131.0: + version "1.20260131.0" + resolved "https://registry.yarnpkg.com/workerd/-/workerd-1.20260131.0.tgz#29ba03e29fc8ff3e41f58ec0c2f0ba84a9cc40e6" + integrity sha512-4zZxOdWeActbRfydQQlj7vZ2ay01AjjNC4K3stjmWC3xZHeXeN3EAROwsWE83SZHhtw4rn18srrhtXoQvQMw3Q== + optionalDependencies: + "@cloudflare/workerd-darwin-64" "1.20260131.0" + "@cloudflare/workerd-darwin-arm64" "1.20260131.0" + "@cloudflare/workerd-linux-64" "1.20260131.0" + "@cloudflare/workerd-linux-arm64" "1.20260131.0" + "@cloudflare/workerd-windows-64" "1.20260131.0" + workerpool@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-3.1.2.tgz#b34e79243647decb174b7481ab5b351dc565c426" @@ -31831,6 +31883,22 @@ wrangler@4.61.0: optionalDependencies: fsevents "~2.3.2" +wrangler@4.62.0: + version "4.62.0" + resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-4.62.0.tgz#cb4e6c7caf16c094e4956611854278e8f641c05b" + integrity sha512-DogP9jifqw85g33BqwF6m21YBW5J7+Ep9IJLgr6oqHU0RkA79JMN5baeWXdmnIWZl+VZh6bmtNtR+5/Djd32tg== + dependencies: + "@cloudflare/kv-asset-handler" "0.4.2" + "@cloudflare/unenv-preset" "2.12.0" + blake3-wasm "2.1.5" + esbuild "0.27.0" + miniflare "4.20260131.0" + path-to-regexp "6.3.0" + unenv "2.0.0-rc.24" + workerd "1.20260131.0" + optionalDependencies: + fsevents "~2.3.2" + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"