Repository navigation
ref(nitro): Move channel-based instrumentation into @sentry/server-utils #24861
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
2eaeef3
bcb71c6
f0ec56f
d18b88c
2dd3477
2f3ae32
791ec03
a7ed926
1ac547f
cce2f0e
eb9579a
1d35ca9
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,9 @@ | ||
| import * as Sentry from '@sentry/node'; | ||
| import { loggingTransport } from '@sentry-internal/node-integration-tests'; | ||
|
|
||
| Sentry.init({ | ||
| dsn: 'https://public@dsn.ingest.sentry.io/1337', | ||
| release: '1.0', | ||
| tracesSampleRate: 1.0, | ||
| transport: loggingTransport, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { sendPortToRunner } from '@sentry-internal/node-integration-tests'; | ||
| import { H3, serve } from 'h3'; | ||
| import { tracingPlugin as h3TracingPlugin } from 'h3/tracing'; | ||
| import { tracingPlugin as srvxTracingPlugin } from 'srvx/tracing'; | ||
| import { createStorage } from 'unstorage'; | ||
| import { withTracing } from 'unstorage/tracing'; | ||
|
|
||
| // Enables the same diagnostics channels a Nitro app publishes, without a Nitro build. | ||
| const storage = withTracing(createStorage()); | ||
|
|
||
| const app = new H3({ plugins: [h3TracingPlugin()] }); | ||
|
|
||
| app.get('/hello/:name', event => `Hello, ${event.context.params.name}!`); | ||
|
|
||
| app.get('/cache', async () => { | ||
| await storage.setItem('cache:key', 'value'); | ||
| return storage.getItem('cache:key'); | ||
| }); | ||
|
|
||
| const server = serve(app, { port: 0, silent: true, plugins: [srvxTracingPlugin()] }); | ||
|
|
||
| await server.ready(); | ||
| sendPortToRunner(new URL(server.url).port); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| import { afterAll, describe, expect, vi } from 'vitest'; | ||
| import { supports } from '../../../utils'; | ||
| import { cleanupChildProcesses, createEsmTests } from '../../../utils/runner'; | ||
|
|
||
| // h3 v2 and srvx are ESM-only and require Node >= 20. | ||
| describe.runIf(supports({ min: 20 }))('nitro integrations', () => { | ||
| afterAll(() => { | ||
| cleanupChildProcesses(); | ||
| }); | ||
|
|
||
| createEsmTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { | ||
| test('creates h3 and srvx spans and sets Server-Timing headers', async () => { | ||
| let traceId: string | undefined; | ||
|
|
||
| const runner = createRunner() | ||
| .expect({ | ||
| span: container => { | ||
| const segment = container.items.find(span => span.is_segment); | ||
| const srvxSpan = container.items.find( | ||
| span => span.attributes['sentry.origin']?.value === 'auto.http.nitro.srvx', | ||
| ); | ||
| const h3Span = container.items.find( | ||
| span => span.attributes['sentry.origin']?.value === 'auto.http.nitro.h3', | ||
| ); | ||
|
|
||
| traceId = segment?.trace_id; | ||
|
|
||
| expect(segment?.name).toBe('GET /hello/:name'); | ||
| expect(segment?.attributes['http.route']).toEqual({ type: 'string', value: '/hello/:name' }); | ||
|
|
||
| // Bun publishes no `http.server.request.start`, so there the srvx span is the segment itself. | ||
| expect(srvxSpan).toMatchObject({ | ||
| status: 'ok', | ||
| attributes: expect.objectContaining({ | ||
| 'sentry.op': { type: 'string', value: 'http.server' }, | ||
| 'http.response.status_code': { type: 'integer', value: 200 }, | ||
| }), | ||
| }); | ||
|
|
||
| expect(h3Span).toMatchObject({ | ||
| name: 'GET /hello/:name', | ||
| parent_span_id: srvxSpan?.span_id, | ||
| status: 'ok', | ||
| attributes: expect.objectContaining({ | ||
| 'sentry.op': { type: 'string', value: 'http.server' }, | ||
| 'http.route': { type: 'string', value: '/hello/:name' }, | ||
| }), | ||
| }); | ||
| }, | ||
| }) | ||
| .start(); | ||
|
|
||
| const port = await vi.waitFor(() => runner.getPort() ?? Promise.reject(new Error('port not ready')), { | ||
| timeout: 30_000, | ||
| }); | ||
| const response = await fetch(`http://localhost:${port}/hello/world`); | ||
| expect(await response.text()).toBe('Hello, world!'); | ||
|
|
||
| await runner.completed(); | ||
|
|
||
| const serverTiming = response.headers.get('server-timing'); | ||
| expect(serverTiming).toContain(`sentry-trace;desc="${traceId}-`); | ||
| expect(serverTiming).toContain('baggage;desc="'); | ||
| }); | ||
|
|
||
| test('creates cache spans for unstorage operations', async () => { | ||
| const runner = createRunner() | ||
| .expect({ | ||
| span: container => { | ||
| const segment = container.items.find(span => span.is_segment); | ||
| const cacheSpans = container.items.filter( | ||
| span => span.attributes['sentry.origin']?.value === 'auto.cache.nitro', | ||
| ); | ||
|
|
||
| expect(segment?.name).toBe('GET /cache'); | ||
| expect(cacheSpans).toEqual([ | ||
| expect.objectContaining({ | ||
| name: 'cache.put', | ||
| status: 'ok', | ||
| attributes: expect.objectContaining({ | ||
| 'sentry.op': { type: 'string', value: 'cache.put' }, | ||
| 'cache.key': { type: 'string', value: 'cache:key' }, | ||
| 'db.operation.name': { type: 'string', value: 'setItem' }, | ||
| 'db.system.name': { type: 'string', value: 'memory' }, | ||
| }), | ||
| }), | ||
| expect.objectContaining({ | ||
| name: 'cache.get', | ||
| status: 'ok', | ||
| attributes: expect.objectContaining({ | ||
| 'sentry.op': { type: 'string', value: 'cache.get' }, | ||
| 'cache.key': { type: 'string', value: 'cache:key' }, | ||
| 'cache.hit': { type: 'boolean', value: true }, | ||
| 'db.operation.name': { type: 'string', value: 'getItem' }, | ||
| }), | ||
| }), | ||
| ]); | ||
| }, | ||
| }) | ||
| .start(); | ||
|
|
||
| expect(await runner.makeRequest('get', '/cache')).toBe('value'); | ||
| await runner.completed(); | ||
| }); | ||
| }); | ||
| }); | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,6 @@ | ||
| import { definePlugin } from 'nitro'; | ||
| import { captureErrorHook } from '../hooks/captureErrorHook'; | ||
| import { captureStorageEvents } from '../hooks/captureStorageEvents'; | ||
| import { captureTracingEvents } from '../hooks/captureTracingEvents'; | ||
|
|
||
| export default definePlugin(nitroApp => { | ||
| nitroApp.hooks.hook('error', captureErrorHook); | ||
|
|
||
| captureTracingEvents(); | ||
| captureStorageEvents(); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| import type { Integration } from '@sentry/core'; | ||
| import { applySdkMetadata } from '@sentry/core'; | ||
| import type { NodeClient, NodeOptions } from '@sentry/node'; | ||
| import { getDefaultIntegrations as getDefaultNodeIntegrations, init as nodeInit } from '@sentry/node'; | ||
| import { getDefaultIntegrations as getDefaultNodeIntegrations, init as nodeInit, nitroIntegration } from '@sentry/node'; | ||
|
|
||
| /** | ||
| * Initializes the Nitro SDK | ||
|
|
@@ -28,5 +28,7 @@ export function init(options: NodeOptions | undefined = {}): NodeClient | undefi | |
| * @returns The default integrations for the Nitro SDK. | ||
| */ | ||
| export function getDefaultIntegrations(options: NodeOptions): Integration[] | undefined { | ||
| return [...getDefaultNodeIntegrations(options)]; | ||
| // `nitroIntegration` is also part of the Node tracing integrations, but add it explicitly so it is | ||
| // always present for the Nitro SDK regardless of that gating (deduped by name when both are added). | ||
| return [...getDefaultNodeIntegrations(options), nitroIntegration()]; | ||
|
Member
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. This adds
Member
Author
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. this is fine here, as we know nitro is relevant when using the nitro sdk I'd say (it is also the same behavior as before, where we always added this)? |
||
| } | ||
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.
Tests skip conventions attribute constants
Low Severity
The new Nitro suite asserts span attributes and ops with string literals such as
sentry.origin,sentry.op,http.route,http.response.status_code,cache.key,cache.hit,http.server,cache.put, andcache.get. Nearby suites import the matching@sentry/conventionsconstants instead. Flagged because the review rules require test assertions on span attributes and ops to use those constants, not string literals.Triggered by project rule: PR Review Guidelines for Cursor Bot
Reviewed by Cursor Bugbot for commit 1d35ca9. Configure here.
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.
that's a good point.