Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions packages/nitro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,13 @@
},
"dependencies": {
"@sentry/core": "10.38.0",
"@sentry/node": "10.38.0"
"@sentry/node": "10.38.0",
"otel-tracing-channel": "^0.2.0"
},
"devDependencies": {
"nitro": "^3.0.1-alpha.1"
"h3": "^2.0.1-rc.13",
"nitro": "^3.0.1-alpha.1",
"srvx": "^0.11.1"
},
"scripts": {
"build": "run-p build:transpile build:types",
Expand Down
2 changes: 1 addition & 1 deletion packages/nitro/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollu
export default [
...makeNPMConfigVariants(
makeBaseNPMConfig({
entrypoints: ['src/index.ts'],
entrypoints: ['src/index.ts', 'src/runtime/plugins/server.ts'],
packageSpecificConfig: {
external: [/^nitro/],
},
Expand Down
12 changes: 12 additions & 0 deletions packages/nitro/src/instruments/instrumentServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Nitro } from 'nitro/types';
import { addPlugin } from '../utils/plugin';
import { createResolver } from '../utils/resolver';

/**
* Sets up the Nitro server instrumentation plugin
* @param nitro - The Nitro instance.
*/
export function instrumentServer(nitro: Nitro): void {
const moduleResolver = createResolver(import.meta.url);
addPlugin(nitro, moduleResolver.resolve('../runtime/plugins/server'));
}
3 changes: 2 additions & 1 deletion packages/nitro/src/module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { NitroModule } from 'nitro/types';
import { instrumentServer } from './instruments/instrumentServer';

/**
* Creates a Nitro module to setup the Sentry SDK.
Expand All @@ -7,7 +8,7 @@ export function createNitroModule(): NitroModule {
return {
name: 'sentry',
setup: nitro => {
// TODO: Setup the Sentry SDK.
instrumentServer(nitro);
},
};
}
5 changes: 5 additions & 0 deletions packages/nitro/src/runtime/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Nitro Runtime

This directory contains the runtime code for Nitro, this includes plugins or any runtime code they may use.

Do not mix runtime code with other code, this directory will be packaged with the SDK and shipped as-is.
165 changes: 165 additions & 0 deletions packages/nitro/src/runtime/plugins/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import {
captureException,
getActiveSpan,
getClient,
getHttpSpanDetailsFromUrlObject,
GLOBAL_OBJ,
httpHeadersToSpanAttributes,
parseStringToURLObject,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
type Span,
SPAN_STATUS_ERROR,
startSpanManual,
} from '@sentry/core';
import type { TracingRequestEvent as H3TracingRequestEvent } from 'h3/tracing';
import { definePlugin } from 'nitro';
import { tracingChannel } from 'otel-tracing-channel';
import type { RequestEvent as SrvxRequestEvent } from 'srvx/tracing';

/**
* Global object with the trace channels
*/
const globalWithTraceChannels = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
__SENTRY_NITRO_HTTP_CHANNELS_INSTRUMENTED__: boolean;
};

/**
* No-op function to satisfy the tracing channel subscribe callbacks
*/
const NOOP = (): void => {};

export default definePlugin(() => {
if (globalWithTraceChannels.__SENTRY_NITRO_HTTP_CHANNELS_INSTRUMENTED__) {
return;
}

setupH3TracingChannels();
setupSrvxTracingChannels();
globalWithTraceChannels.__SENTRY_NITRO_HTTP_CHANNELS_INSTRUMENTED__ = true;
});

function onTraceEnd(data: { span?: Span }): void {
data.span?.end();
}

function onTraceError(data: { span?: Span; error: unknown }): void {
captureException(data.error);
data.span?.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
data.span?.end();
}

function setupH3TracingChannels(): void {
const h3Channel = tracingChannel<H3TracingRequestEvent>('h3.fetch', data => {
const parsedUrl = parseStringToURLObject(data.event.url.href);
const [spanName, urlAttributes] = getHttpSpanDetailsFromUrlObject(parsedUrl, 'server', 'auto.http.nitro.h3', {
method: data.event.req.method,
});

return startSpanManual(
{
name: spanName,
attributes: {
...urlAttributes,
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: data?.type === 'middleware' ? 'middleware.nitro' : 'http.server',
},
},
s => s,
);
});

h3Channel.subscribe({
start: NOOP,
asyncStart: NOOP,
end: NOOP,
asyncEnd: onTraceEnd,
error: onTraceError,
});
}

function setupSrvxTracingChannels(): void {
// Store the parent span for all middleware and fetch to share
// This ensures they all appear as siblings in the trace
let requestParentSpan: Span | null = null;

const fetchChannel = tracingChannel<SrvxRequestEvent>('srvx.fetch', data => {
const parsedUrl = data.request._url ? parseStringToURLObject(data.request._url.href) : undefined;
const [spanName, urlAttributes] = getHttpSpanDetailsFromUrlObject(parsedUrl, 'server', 'auto.http.nitro.srvx', {
method: data.request.method,
});

const sendDefaultPii = getClient()?.getOptions().sendDefaultPii ?? false;
const headerAttributes = httpHeadersToSpanAttributes(
Object.fromEntries(data.request.headers.entries()),
sendDefaultPii,
);

return startSpanManual(
{
name: spanName,
attributes: {
...urlAttributes,
...headerAttributes,
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: data.middleware ? 'middleware.nitro' : 'http.server',
'server.port': data.server.options.port,
},
// Use the same parent span as middleware to make them siblings
parentSpan: requestParentSpan || undefined,
},
span => span,
);
});

// Subscribe to events (span already created in bindStore)
fetchChannel.subscribe({
start: () => {},
asyncStart: () => {},
end: () => {},
asyncEnd: data => {
// data.span?.setAttribute('http.response.status_code', data.result.);
onTraceEnd(data);

// Reset parent span reference after the fetch handler completes
// This ensures each request gets a fresh parent span capture
requestParentSpan = null;
},
error: data => {
onTraceError(data);
// Reset parent span reference on error too
requestParentSpan = null;
},
});

const middlewareChannel = tracingChannel<SrvxRequestEvent>('srvx.middleware', data => {
// For the first middleware, capture the current parent span
if (data.middleware?.index === 0) {
requestParentSpan = getActiveSpan() || null;
}

const parsedUrl = data.request._url ? parseStringToURLObject(data.request._url.href) : undefined;
const [, urlAttributes] = getHttpSpanDetailsFromUrlObject(parsedUrl, 'server', 'auto.http.nitro.srvx.middleware', {
method: data.request.method,
});

// Create span as a child of the original parent, not the previous middleware
return startSpanManual(
{
name: `${data.middleware?.handler.name ?? 'unknown'} - ${data.request.method} ${data.request._url?.pathname}`,
attributes: {
...urlAttributes,
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nitro',
},
parentSpan: requestParentSpan || undefined,
},
span => span,
);
});

// Subscribe to events (span already created in bindStore)
middlewareChannel.subscribe({
start: () => {},
asyncStart: () => {},
end: () => {},
asyncEnd: onTraceEnd,
error: onTraceError,
});
}
Loading
Loading