Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
123bbfd
feat: added h3 channel
logaretm Dec 31, 2025
c5d3cbe
feat: added srvx channel and enhanced attr collection
logaretm Dec 31, 2025
39b0765
fix: ensure runtime plugins are present in the dist
logaretm Jan 27, 2026
da1fe6a
fix: tracing channel name
logaretm Feb 5, 2026
44069ff
ref: use only one global flag for channel installation
logaretm Feb 6, 2026
1efb3b5
feat: handle http errors
logaretm Feb 9, 2026
97a5c62
feat: added http status code handling
logaretm Feb 9, 2026
f694ebe
feat: force enable tracing for the user
logaretm Feb 9, 2026
48a8c54
fix: tracing config may have changed
logaretm Feb 9, 2026
5717354
fix: correctly enable the config
logaretm Feb 9, 2026
361aae6
fix: configure externals correctly
logaretm Feb 9, 2026
945ba10
test: added e2e tests
logaretm Feb 9, 2026
5c71f67
test: test isolation scope
logaretm Feb 9, 2026
67c6a2c
feat: added server timing headers
logaretm Feb 9, 2026
cacda7e
feat: use vite mode for better test coverage
logaretm Feb 9, 2026
33822dd
fix: update channel names and always end the spans
logaretm Feb 9, 2026
e4b8cd3
feat: ensure trace channel spans has correct origins
logaretm Feb 9, 2026
f5c0e00
test: add middleware error test
logaretm Feb 9, 2026
2050ad2
feat: route parameterization
logaretm Feb 9, 2026
1c18498
fix: set headers before they get frozen
logaretm Feb 12, 2026
0334770
fix: update config name
logaretm Apr 15, 2026
88f66e7
chore: pin versions properly
logaretm Apr 15, 2026
76cb453
chore: add canary entry
logaretm Apr 15, 2026
45229f9
fix: added error capturing mechanisms
logaretm Apr 16, 2026
647e65a
chore: nitro hooks are typed properly now
logaretm Apr 16, 2026
9299ba9
fix: ensure we rename the root span as well
logaretm Apr 16, 2026
2ba552a
refactor: use WeakMap for per-request parent span management in traci…
logaretm Apr 16, 2026
fef7171
fix: set paramaterized route path attributes
logaretm Apr 16, 2026
33b11e9
fix: use url parsing util instead
logaretm Apr 16, 2026
2491ade
fix: use vendored tracingChannel and fix span ending in nitro tracing…
logaretm Apr 16, 2026
12ca3f5
fix: use subpath export
logaretm Apr 16, 2026
6b8d4d7
fix: externalize subexports
logaretm Apr 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ jobs:
- test-application: 'nestjs-microservices'
build-command: 'test:build-latest'
label: 'nestjs-microservices (latest)'
- test-application: 'nitro-3'
build-command: 'test:build-canary'
label: 'nitro-3 (canary)'
Comment thread
cursor[bot] marked this conversation as resolved.

steps:
- name: Check out current commit
Expand Down
2 changes: 2 additions & 0 deletions dev-packages/e2e-tests/test-applications/nitro-3/.npmrc
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
11 changes: 11 additions & 0 deletions dev-packages/e2e-tests/test-applications/nitro-3/index.html
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,
});
29 changes: 29 additions & 0 deletions dev-packages/e2e-tests/test-applications/nitro-3/package.json
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",
Comment thread
logaretm marked this conversation as resolved.
"test:assert": "pnpm test"
Comment thread
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');
}
});
10 changes: 10 additions & 0 deletions dev-packages/e2e-tests/test-applications/nitro-3/src/main.ts
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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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!

});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flaky 404 test has race condition in assertion

Medium Severity

The "Does not send 404 errors" test asserts errorReceived is false immediately after the request completes, with no wait for potential error events to propagate through the proxy server. Even if the server incorrectly sent a 404 error event, it wouldn't have arrived by the time the assertion runs — making this test incapable of catching regressions. A more reliable pattern is to send a subsequent known-good request, wait for its transaction event, and then assert no error was received.

Fix in Cursor Fix in Web

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');
});
Loading