-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(hono): Add basic instrumentation for Node runtime #19817
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
16fa689
b09ca39
377c5cf
f8e74c9
e0d80d4
4b860e5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| // Sentry is initialized by the @sentry/hono/node middleware in scenario.mjs |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { serve } from '@hono/node-server'; | ||
| import { sentry } from '@sentry/hono/node'; | ||
| import { loggingTransport, sendPortToRunner } from '@sentry-internal/node-integration-tests'; | ||
| import { Hono } from 'hono'; | ||
|
|
||
| const app = new Hono(); | ||
|
|
||
| app.use( | ||
| sentry(app, { | ||
| dsn: 'https://public@dsn.ingest.sentry.io/1337', | ||
| tracesSampleRate: 1.0, | ||
| transport: loggingTransport, | ||
| }), | ||
| ); | ||
|
|
||
| app.get('/', c => { | ||
| return c.text('Hello from Hono on Node!'); | ||
| }); | ||
|
|
||
| app.get('/hello/:name', c => { | ||
| const name = c.req.param('name'); | ||
| return c.text(`Hello, ${name}!`); | ||
| }); | ||
|
|
||
| app.get('/error/:param', () => { | ||
| throw new Error('Test error from Hono app'); | ||
| }); | ||
|
|
||
| serve({ fetch: app.fetch, port: 0 }, info => { | ||
| sendPortToRunner(info.port); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import { afterAll, describe, expect } from 'vitest'; | ||
| import { cleanupChildProcesses, createEsmAndCjsTests } from '../../utils/runner'; | ||
|
|
||
| describe('hono-sdk (Node)', () => { | ||
| afterAll(() => { | ||
| cleanupChildProcesses(); | ||
| }); | ||
|
|
||
| createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { | ||
| test('creates a transaction for a basic GET request', async () => { | ||
| const runner = createRunner() | ||
| .expect({ | ||
| transaction: { | ||
| transaction: 'GET /', | ||
| contexts: { | ||
| trace: { | ||
| op: 'http.server', | ||
| status: 'ok', | ||
| }, | ||
| }, | ||
| }, | ||
| }) | ||
| .start(); | ||
| runner.makeRequest('get', '/'); | ||
| await runner.completed(); | ||
| }); | ||
|
|
||
| test('creates a transaction with a parametrized route name', async () => { | ||
| const runner = createRunner() | ||
| .expect({ | ||
| transaction: { | ||
| transaction: 'GET /hello/:name', | ||
| transaction_info: { | ||
| source: 'route', | ||
| }, | ||
| contexts: { | ||
| trace: { | ||
| op: 'http.server', | ||
| status: 'ok', | ||
| }, | ||
| }, | ||
| }, | ||
| }) | ||
| .start(); | ||
| runner.makeRequest('get', '/hello/world'); | ||
| await runner.completed(); | ||
| }); | ||
|
|
||
| test('captures an error with the correct mechanism', async () => { | ||
| const runner = createRunner() | ||
| .ignore('transaction') | ||
| .expect({ | ||
| event: { | ||
| exception: { | ||
| values: [ | ||
| { | ||
| type: 'Error', | ||
| value: 'Test error from Hono app', | ||
| mechanism: { | ||
| type: 'auto.faas.hono.error_handler', | ||
| handled: false, | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
| transaction: 'GET /error/:param', | ||
| }, | ||
| }) | ||
| .start(); | ||
| runner.makeRequest('get', '/error/param-123', { expectError: true }); | ||
| await runner.completed(); | ||
| }); | ||
|
|
||
| test('creates a transaction with internal_error status when an error occurs', async () => { | ||
| const runner = createRunner() | ||
| .ignore('event') | ||
| .expect({ | ||
| transaction: { | ||
| transaction: 'GET /error/:param', | ||
| contexts: { | ||
| trace: { | ||
| op: 'http.server', | ||
| status: 'internal_error', | ||
| data: expect.objectContaining({ | ||
| 'http.response.status_code': 500, | ||
| }), | ||
| }, | ||
| }, | ||
| }, | ||
| }) | ||
| .start(); | ||
| runner.makeRequest('get', '/error/param-456', { expectError: true }); | ||
| await runner.completed(); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export { sentry } from './node/middleware'; | ||
|
|
||
| export * from '@sentry/node'; | ||
|
|
||
| export { init } from './node/sdk'; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { type BaseTransportOptions, debug, type Options } from '@sentry/core'; | ||
| import { init } from './sdk'; | ||
| import type { Hono, MiddlewareHandler } from 'hono'; | ||
| import { patchAppUse } from '../shared/patchAppUse'; | ||
| import { requestHandler, responseHandler } from '../shared/middlewareHandlers'; | ||
|
|
||
| export interface HonoNodeOptions extends Options<BaseTransportOptions> {} | ||
|
|
||
| /** | ||
| * Sentry middleware for Hono running in a Node runtime environment. | ||
| */ | ||
| export const sentry = (app: Hono, options: HonoNodeOptions | undefined = {}): MiddlewareHandler => { | ||
| const isDebug = options.debug; | ||
|
|
||
| isDebug && debug.log('Initialized Sentry Hono middleware (Node)'); | ||
|
|
||
| init(options); | ||
|
|
||
| patchAppUse(app); | ||
|
|
||
| return async (context, next) => { | ||
| requestHandler(context); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The Suggested FixThe Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||
|
|
||
| await next(); // Handler runs in between Request above ⤴ and Response below ⤵ | ||
|
|
||
| responseHandler(context); | ||
| }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wrong
|
||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import type { Client, Integration } from '@sentry/core'; | ||
| import { applySdkMetadata, getIntegrationsToSetup } from '@sentry/core'; | ||
| import { init as initNode } from '@sentry/node'; | ||
| import type { HonoNodeOptions } from './middleware'; | ||
| import { filterHonoIntegration } from '../shared/filterHonoIntegration'; | ||
|
|
||
| /** | ||
| * Initializes Sentry for Hono running in a Node runtime environment. | ||
| * | ||
| * In general, it is recommended to initialize Sentry via the `sentry()` middleware, as it sets up everything by default and calls `init` internally. | ||
| * | ||
| * When manually calling `init`, add the `honoIntegration` to the `integrations` array to set up the Hono integration. | ||
| */ | ||
| export function init(options: HonoNodeOptions): Client | undefined { | ||
| applySdkMetadata(options, 'hono', ['hono', 'node']); | ||
|
|
||
| const { integrations: userIntegrations } = options; | ||
|
|
||
| // Remove Hono from the SDK defaults to prevent double instrumentation: @sentry/node | ||
| const filteredOptions: HonoNodeOptions = { | ||
| ...options, | ||
| integrations: Array.isArray(userIntegrations) | ||
| ? (defaults: Integration[]) => | ||
| getIntegrationsToSetup({ | ||
| defaultIntegrations: defaults.filter(filterHonoIntegration), | ||
| integrations: userIntegrations, // user's explicit Hono integration is preserved | ||
| }) | ||
| : typeof userIntegrations === 'function' | ||
| ? (defaults: Integration[]) => userIntegrations(defaults.filter(filterHonoIntegration)) | ||
| : (defaults: Integration[]) => defaults.filter(filterHonoIntegration), | ||
| }; | ||
|
|
||
| return initNode(filteredOptions); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import type { Integration } from '@sentry/core'; | ||
|
|
||
| export const filterHonoIntegration = (integration: Integration): boolean => integration.name !== 'Hono'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,10 @@ | ||
| import { getIsolationScope } from '@sentry/cloudflare'; | ||
| import { | ||
| getActiveSpan, | ||
| getClient, | ||
| getDefaultIsolationScope, | ||
| getIsolationScope, | ||
| getRootSpan, | ||
| SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, | ||
| updateSpanName, | ||
| winterCGRequestToRequestData, | ||
| } from '@sentry/core'; | ||
|
|
@@ -32,7 +33,11 @@ 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)}`); | ||
| activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); | ||
|
|
||
| const rootSpan = getRootSpan(activeSpan); | ||
| updateSpanName(rootSpan, `${context.req.method} ${routePath(context)}`); | ||
| rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect
|
||
| } | ||
|
|
||
| getIsolationScope().setTransactionName(`${context.req.method} ${routePath(context)}`); | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
l/q: Do you think it would make sense to re-export
initand addapplySdkMetadata(opts, 'hono', ['hono', 'node']);here on top?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We currently don't document this way of setting it up (calling
initmanually) but that's a good idea!