-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(nitro): Instrument HTTP Server #19225
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
123bbfd
c5d3cbe
39b0765
da1fe6a
44069ff
1efb3b5
97a5c62
f694ebe
48a8c54
5717354
361aae6
945ba10
5c71f67
67c6a2c
cacda7e
33822dd
e4b8cd3
f5c0e00
2050ad2
1c18498
0334770
88f66e7
76cb453
45229f9
647e65a
9299ba9
2ba552a
fef7171
33b11e9
2491ade
12ca3f5
6b8d4d7
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,2 @@ | ||
| @sentry:registry=http://127.0.0.1:4873 | ||
| @sentry-internal:registry=http://127.0.0.1:4873 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <title>Nitro E2E Test</title> | ||
| </head> | ||
| <body> | ||
| <h1>Nitro E2E Test App</h1> | ||
| <script type="module" src="/src/main.ts"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import * as Sentry from '@sentry/nitro'; | ||
|
|
||
| Sentry.init({ | ||
| environment: 'qa', // dynamic sampling bias to keep transactions | ||
| dsn: process.env.E2E_TEST_DSN, | ||
| tunnel: `http://localhost:3031/`, // proxy server | ||
| tracesSampleRate: 1, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| { | ||
| "name": "nitro-3", | ||
| "version": "1.0.0", | ||
| "private": true, | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "vite build", | ||
| "start": "PORT=3030 NODE_OPTIONS='--import ./instrument.mjs' node .output/server/index.mjs", | ||
| "clean": "npx rimraf node_modules pnpm-lock.yaml .output", | ||
| "test": "playwright test", | ||
| "test:build": "pnpm install && pnpm build", | ||
|
logaretm marked this conversation as resolved.
|
||
| "test:assert": "pnpm test" | ||
|
logaretm marked this conversation as resolved.
|
||
| }, | ||
| "dependencies": { | ||
| "@sentry/browser": "latest || *", | ||
| "@sentry/nitro": "latest || *" | ||
| }, | ||
| "devDependencies": { | ||
| "@playwright/test": "~1.56.0", | ||
| "@sentry-internal/test-utils": "link:../../../test-utils", | ||
| "@sentry/core": "latest || *", | ||
| "nitro": "^3.0.260415-beta", | ||
| "rolldown": "latest", | ||
| "vite": "latest" | ||
| }, | ||
| "volta": { | ||
| "extends": "../../package.json" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { getPlaywrightConfig } from '@sentry-internal/test-utils'; | ||
|
|
||
| const config = getPlaywrightConfig({ | ||
| startCommand: `pnpm start`, | ||
| }); | ||
|
|
||
| export default config; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { defineHandler } from 'nitro/h3'; | ||
|
|
||
| export default defineHandler(() => { | ||
| return { status: 'ok' }; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { defineHandler } from 'nitro/h3'; | ||
|
|
||
| export default defineHandler(() => { | ||
| throw new Error('This is a test error'); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { getDefaultIsolationScope, setTag } from '@sentry/core'; | ||
| import { defineHandler } from 'nitro/h3'; | ||
|
|
||
| export default defineHandler(() => { | ||
| setTag('my-isolated-tag', true); | ||
| // Check if the tag leaked into the default (global) isolation scope | ||
| setTag('my-global-scope-isolated-tag', getDefaultIsolationScope().getScopeData().tags['my-isolated-tag']); | ||
|
|
||
| throw new Error('Isolation test error'); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { startSpan } from '@sentry/nitro'; | ||
| import { defineHandler } from 'nitro/h3'; | ||
|
|
||
| export default defineHandler(() => { | ||
| startSpan({ name: 'db.select', op: 'db' }, () => { | ||
| // simulate a select query | ||
| }); | ||
|
|
||
| startSpan({ name: 'db.insert', op: 'db' }, () => { | ||
| startSpan({ name: 'db.serialize', op: 'serialize' }, () => { | ||
| // simulate serializing data before insert | ||
| }); | ||
| }); | ||
|
|
||
| return { status: 'ok', nesting: true }; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { defineHandler } from 'nitro/h3'; | ||
|
|
||
| export default defineHandler(event => { | ||
| const id = event.req.url; | ||
| return { id }; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { defineHandler } from 'nitro/h3'; | ||
|
|
||
| export default defineHandler(() => { | ||
| return { status: 'ok', transaction: true }; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { defineHandler, getQuery, setResponseHeader } from 'nitro/h3'; | ||
|
|
||
| export default defineHandler(event => { | ||
| setResponseHeader(event, 'x-sentry-test-middleware', 'executed'); | ||
|
|
||
| const query = getQuery(event); | ||
| if (query['middleware-error'] === '1') { | ||
| throw new Error('Middleware error'); | ||
| } | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import * as Sentry from '@sentry/browser'; | ||
|
|
||
| // Let's us test trace propagation | ||
| Sentry.init({ | ||
| environment: 'qa', | ||
| dsn: 'https://public@dsn.ingest.sentry.io/1337', | ||
| tunnel: 'http://localhost:3031/', // proxy server | ||
| integrations: [Sentry.browserTracingIntegration()], | ||
| tracesSampleRate: 1.0, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { startEventProxyServer } from '@sentry-internal/test-utils'; | ||
|
|
||
| startEventProxyServer({ | ||
| port: 3031, | ||
| proxyServerName: 'nitro-3', | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
| import { waitForError } from '@sentry-internal/test-utils'; | ||
|
|
||
| test('Sends an error event to Sentry', async ({ request }) => { | ||
| const errorEventPromise = waitForError('nitro-3', event => { | ||
| return !event.type && !!event.exception?.values?.some(v => v.value === 'This is a test error'); | ||
| }); | ||
|
|
||
| await request.get('/api/test-error'); | ||
|
|
||
| const errorEvent = await errorEventPromise; | ||
|
|
||
| // Nitro wraps thrown errors in an HTTPError with .cause, producing a chained exception | ||
| expect(errorEvent.exception?.values).toHaveLength(2); | ||
|
|
||
| // The innermost exception (values[0]) is the original thrown error | ||
| expect(errorEvent.exception?.values?.[0]?.type).toBe('Error'); | ||
| expect(errorEvent.exception?.values?.[0]?.value).toBe('This is a test error'); | ||
| expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual( | ||
| expect.objectContaining({ | ||
| handled: false, | ||
| type: 'auto.function.nitro.captureErrorHook', | ||
| }), | ||
| ); | ||
|
|
||
| // The outermost exception (values[1]) is the HTTPError wrapper | ||
| expect(errorEvent.exception?.values?.[1]?.type).toBe('HTTPError'); | ||
| expect(errorEvent.exception?.values?.[1]?.value).toBe('This is a test error'); | ||
| }); | ||
|
|
||
| test('Does not send 404 errors to Sentry', async ({ request }) => { | ||
| let errorReceived = false; | ||
|
|
||
| void waitForError('nitro-3', event => { | ||
| if (!event.type) { | ||
| errorReceived = true; | ||
| return true; | ||
| } | ||
| return false; | ||
| }); | ||
|
|
||
| await request.get('/api/non-existent-route'); | ||
|
|
||
| expect(errorReceived).toBe(false); | ||
|
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. What we do in other e2e-tests for such tests is to have a separate flush endpoint that is called after the error endpoint to ensure that all events have been sent (e.g. nestjs, cloudflare), so I think it would be good to switch to that pattern here too
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. Makes sense, I will merge this into the base branch but will make sure to do that! thanks! |
||
| }); | ||
|
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. Flaky 404 test has race condition in assertionMedium Severity The "Does not send 404 errors" test asserts Triggered by project rule: PR Review Guidelines for Cursor Bot Reviewed by Cursor Bugbot for commit c345104. Configure here. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
| import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; | ||
|
|
||
| test('Isolation scope prevents tag leaking between requests', async ({ request }) => { | ||
| const transactionEventPromise = waitForTransaction('nitro-3', event => { | ||
| return event?.transaction === 'GET /api/test-isolation/:id'; | ||
| }); | ||
|
|
||
| const errorPromise = waitForError('nitro-3', event => { | ||
| return !event.type && !!event.exception?.values?.some(v => v.value === 'Isolation test error'); | ||
| }); | ||
|
|
||
| await request.get('/api/test-isolation/1').catch(() => { | ||
| // noop - route throws | ||
| }); | ||
|
|
||
| const transactionEvent = await transactionEventPromise; | ||
| const error = await errorPromise; | ||
|
|
||
| // Assert that isolation scope works properly | ||
| expect(error.tags?.['my-isolated-tag']).toBe(true); | ||
| expect(error.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); | ||
| expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true); | ||
| expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
| import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; | ||
|
|
||
| test('Creates middleware spans for requests', async ({ request }) => { | ||
| const transactionEventPromise = waitForTransaction('nitro-3', event => { | ||
| return event?.transaction === 'GET /api/test-transaction'; | ||
| }); | ||
|
|
||
| const response = await request.get('/api/test-transaction'); | ||
|
|
||
| expect(response.headers()['x-sentry-test-middleware']).toBe('executed'); | ||
|
|
||
| const transactionEvent = await transactionEventPromise; | ||
|
|
||
| // h3 middleware spans have origin auto.http.nitro.h3 and op middleware.nitro | ||
| const h3MiddlewareSpans = transactionEvent.spans?.filter( | ||
| span => span.origin === 'auto.http.nitro.h3' && span.op === 'middleware.nitro', | ||
| ); | ||
| expect(h3MiddlewareSpans?.length).toBeGreaterThanOrEqual(1); | ||
| }); | ||
|
|
||
| test('Captures errors thrown in middleware with error status on span', async ({ request }) => { | ||
| const errorEventPromise = waitForError('nitro-3', event => { | ||
| return !event.type && !!event.exception?.values?.some(v => v.value === 'Middleware error'); | ||
| }); | ||
|
|
||
| const transactionEventPromise = waitForTransaction('nitro-3', event => { | ||
| return event?.transaction === 'GET /api/test-transaction' && event?.contexts?.trace?.status === 'internal_error'; | ||
| }); | ||
|
|
||
| await request.get('/api/test-transaction?middleware-error=1'); | ||
|
|
||
| const errorEvent = await errorEventPromise; | ||
| expect(errorEvent.exception?.values?.some(v => v.value === 'Middleware error')).toBe(true); | ||
|
|
||
| const transactionEvent = await transactionEventPromise; | ||
|
|
||
| // The transaction span should have error status | ||
| expect(transactionEvent.contexts?.trace?.status).toBe('internal_error'); | ||
| }); |


Uh oh!
There was an error while loading. Please reload this page.