-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(nitro): Nitro SDK #19224
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
base: develop
Are you sure you want to change the base?
feat(nitro): Nitro SDK #19224
Changes from all commits
9ee8879
c566745
14472f5
354a69d
b7ef179
626f36d
5cd5d60
fd9ed65
a071048
cd27a72
e3209ec
f70d3f4
f1bc8e2
fa580dd
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", | ||
| "test:assert": "pnpm test" | ||
|
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. Missing
|
||
| }, | ||
| "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); | ||
| }); | ||
|
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. Negative test never fails due to race conditionMedium Severity The "Does not send 404 errors" test Triggered by project rule: PR Review Guidelines for Cursor Bot Reviewed by Cursor Bugbot for commit 4cd6415. 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'); | ||
| }); |


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.
Missing
test:build-canaryscript breaks canary CIHigh Severity
The canary workflow references
test:build-canaryas thebuild-commandfor thenitro-3test application, but thepackage.jsonfornitro-3only definestest:build— there is notest:build-canaryscript. The workflow step executesyarn ${{ matrix.build-command }}, which will fail. Other test applications likenuxt-3andnuxt-4correctly definetest:build-canaryscripts that install canary/nightly versions of their framework dependencies.Additional Locations (1)
dev-packages/e2e-tests/test-applications/nitro-3/package.json#L5-L13Reviewed by Cursor Bugbot for commit 4cd6415. Configure here.