Skip to content
Merged
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
35 changes: 15 additions & 20 deletions packages/nestjs/src/integrations/orchestrion-subscriber.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as diagnosticsChannel from 'node:diagnostics_channel';
import { debug, startInactiveSpan, waitForTracingChannelBinding } from '@sentry/core';
import type { OrchestrionChannelContext } from '@sentry/server-utils';
import { bindTracingChannelToSpan } from '@sentry/server-utils';
import { nestjsChannels as CHANNELS } from '@sentry/server-utils/orchestrion/config';
import { DEBUG_BUILD } from '../debug-build';
Expand All @@ -21,18 +22,6 @@ import { getAppCreationSpanOptions, wrapRequestContextHandler, wrapRouteHandler

const NOOP = (): void => {};

/**
* The orchestrion tracing-channel context. `arguments` is the live call args
* array; `result` is the return value, which an `end` handler may reassign to
* substitute it (`traceSync`/`tracePromise` always return `ctx.result`).
*/
interface ChannelContext {
arguments: unknown[];
moduleVersion?: string;
result?: unknown;
error?: unknown;
}

/**
* Subscribe to a decorator channel (`Injectable`/`Catch`).
*
Expand All @@ -42,7 +31,7 @@ interface ChannelContext {
* spans later.
*/
function subscribeDecoratorChannel<T>(channelName: string, patch: (target: T) => void): void {
diagnosticsChannel.tracingChannel<ChannelContext>(channelName).subscribe({
diagnosticsChannel.tracingChannel<OrchestrionChannelContext>(channelName).subscribe({
start(data) {
const target = data.arguments?.[0] as T | undefined;
if (target) {
Expand Down Expand Up @@ -90,8 +79,11 @@ function makeProcessorDecorator(original: AnyFn, queueName: string | undefined):
* whatever `end` leaves there. `wrap` receives the original decorator and the
* channel context (for the factory's args, e.g. the BullMQ queue name).
*/
function subscribeFactoryDecorator(channelName: string, wrap: (decorator: AnyFn, data: ChannelContext) => AnyFn): void {
diagnosticsChannel.tracingChannel<ChannelContext>(channelName).subscribe({
function subscribeFactoryDecorator(
channelName: string,
wrap: (decorator: AnyFn, data: OrchestrionChannelContext) => AnyFn,
): void {
diagnosticsChannel.tracingChannel<OrchestrionChannelContext>(channelName).subscribe({
start: NOOP,
end(data) {
const decorator = data.result;
Expand Down Expand Up @@ -134,10 +126,13 @@ export function subscribeToNestChannels(): void {
// calls below stay synchronous because the decorator channels fire at
// module-load time, which a deferred subscription could miss.
waitForTracingChannelBinding(() => {
bindTracingChannelToSpan(diagnosticsChannel.tracingChannel<ChannelContext>(CHANNELS.NESTJS_APP_CREATION), data => {
const moduleCls = data.arguments?.[0] as { name?: string } | undefined;
return startInactiveSpan(getAppCreationSpanOptions(data.moduleVersion, moduleCls?.name));
});
bindTracingChannelToSpan(
diagnosticsChannel.tracingChannel<OrchestrionChannelContext>(CHANNELS.NESTJS_APP_CREATION),
data => {
const moduleCls = data.arguments?.[0] as { name?: string } | undefined;
return startInactiveSpan(getAppCreationSpanOptions(data.moduleVersion, moduleCls?.name));
},
);
});

// request_context + request_handler. `RouterExecutionContext.create`
Expand All @@ -146,7 +141,7 @@ export function subscribeToNestChannels(): void {
// (-> handler span per call) and `end` reassigns `data.result` to
// replace the returned handler (-> request_context span per request).
const routerMeta = new WeakMap<object, { instanceName: string; callbackName: string; moduleVersion?: string }>();
diagnosticsChannel.tracingChannel<ChannelContext>(CHANNELS.NESTJS_ROUTER_CONTEXT).subscribe({
diagnosticsChannel.tracingChannel<OrchestrionChannelContext>(CHANNELS.NESTJS_ROUTER_CONTEXT).subscribe({
start(data) {
const instance = data.arguments?.[0] as { constructor?: { name?: string } } | undefined;
const callback = data.arguments?.[1];
Expand Down
39 changes: 13 additions & 26 deletions packages/nestjs/test/integrations/orchestrion-subscriber.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
spanToJSON,
} from '@sentry/core';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { OrchestrionChannelContext } from '@sentry/server-utils';
import { nestjsChannels as CHANNELS } from '@sentry/server-utils/orchestrion/config';
import { subscribeToNestChannels } from '../../src/integrations/orchestrion-subscriber';

Expand Down Expand Up @@ -94,13 +95,6 @@ function installTestAsyncContextStrategy(): void {
});
}

interface NestFactoryCreateData {
arguments: unknown[];
moduleVersion?: string;
result?: unknown;
error?: unknown;
}

describe('NestJS orchestrion subscriber: app_creation', () => {
afterEach(() => {
setAsyncContextStrategy(undefined);
Expand All @@ -112,12 +106,12 @@ describe('NestJS orchestrion subscriber: app_creation', () => {
// `data._sentrySpan`
function captureSpan(): { getSpan: () => Span | undefined } {
let span: Span | undefined;
const grab = (data: NestFactoryCreateData): void => {
const grab = (data: OrchestrionChannelContext): void => {
span ??= (data as { _sentrySpan?: Span })._sentrySpan;
};
// The raw node `tracingChannel` type wants all five handlers; only
// `end`/`asyncEnd` carry the bound span by the time it settles.
tracingChannel<NestFactoryCreateData>(CHANNELS.NESTJS_APP_CREATION).subscribe({
tracingChannel<OrchestrionChannelContext>(CHANNELS.NESTJS_APP_CREATION).subscribe({
start: () => undefined,
asyncStart: () => undefined,
asyncEnd: grab,
Expand All @@ -133,7 +127,7 @@ describe('NestJS orchestrion subscriber: app_creation', () => {
subscribeToNestChannels();

const { getSpan } = captureSpan();
const channel = tracingChannel<NestFactoryCreateData>(CHANNELS.NESTJS_APP_CREATION);
const channel = tracingChannel<OrchestrionChannelContext>(CHANNELS.NESTJS_APP_CREATION);

class AppModule {}
await channel.tracePromise(async () => ({ app: true }), { arguments: [AppModule], moduleVersion: '10.4.1' });
Expand All @@ -160,7 +154,7 @@ describe('NestJS orchestrion subscriber: app_creation', () => {
subscribeToNestChannels();

const { getSpan } = captureSpan();
const channel = tracingChannel<NestFactoryCreateData>(CHANNELS.NESTJS_APP_CREATION);
const channel = tracingChannel<OrchestrionChannelContext>(CHANNELS.NESTJS_APP_CREATION);

await channel.tracePromise(async () => ({ app: true }), { arguments: [] });

Expand All @@ -173,13 +167,6 @@ describe('NestJS orchestrion subscriber: app_creation', () => {

type AnyFn = (this: unknown, ...args: unknown[]) => unknown;

interface RouterCreateData {
arguments: unknown[];
moduleVersion?: string;
result?: unknown;
error?: unknown;
}

describe('NestJS orchestrion subscriber: request_context / request_handler', () => {
afterEach(() => {
setAsyncContextStrategy(undefined);
Expand All @@ -195,10 +182,10 @@ describe('NestJS orchestrion subscriber: request_context / request_handler', ()
instance: object,
callback: AnyFn,
moduleVersion: string | undefined,
makeHandler: (data: RouterCreateData) => AnyFn,
makeHandler: (data: OrchestrionChannelContext) => AnyFn,
): { effectiveHandler: AnyFn; wrappedCallback: AnyFn } {
const channel = tracingChannel<RouterCreateData>(CHANNELS.NESTJS_ROUTER_CONTEXT);
const data: RouterCreateData = { arguments: [instance, callback], moduleVersion };
const channel = tracingChannel<OrchestrionChannelContext>(CHANNELS.NESTJS_ROUTER_CONTEXT);
const data: OrchestrionChannelContext = { arguments: [instance, callback], moduleVersion };
channel.traceSync(() => makeHandler(data), data);
return { effectiveHandler: data.result as AnyFn, wrappedCallback: data.arguments[1] as AnyFn };
}
Expand Down Expand Up @@ -340,7 +327,7 @@ describe('NestJS orchestrion subscriber: @Injectable (middleware/guard/pipe/inte
// Fire the @Injectable channel against `target` (as if its decorator arrow
// ran), so the subscriber's `start` patches `target.prototype`.
function applyInjectable(target: object): void {
tracingChannel<{ arguments: unknown[] }>(CHANNELS.NESTJS_INJECTABLE).traceSync(() => undefined, {
tracingChannel<OrchestrionChannelContext>(CHANNELS.NESTJS_INJECTABLE).traceSync(() => undefined, {
arguments: [target],
});
}
Expand Down Expand Up @@ -643,7 +630,7 @@ describe('NestJS orchestrion subscriber: @Catch (exception filter)', () => {
});

function applyCatch(target: object): void {
tracingChannel<{ arguments: unknown[] }>(CHANNELS.NESTJS_CATCH).traceSync(() => undefined, {
tracingChannel<OrchestrionChannelContext>(CHANNELS.NESTJS_CATCH).traceSync(() => undefined, {
arguments: [target],
});
}
Expand Down Expand Up @@ -697,7 +684,7 @@ describe('NestJS orchestrion subscriber: @Catch (exception filter)', () => {
// @Catch first. Because the two passes use separate patched-flags, both
// must wrap their own methods regardless of which channel fires first.
function fireInjectable(target: object): void {
tracingChannel<{ arguments: unknown[] }>(CHANNELS.NESTJS_INJECTABLE).traceSync(() => undefined, {
tracingChannel<OrchestrionChannelContext>(CHANNELS.NESTJS_INJECTABLE).traceSync(() => undefined, {
arguments: [target],
});
}
Expand Down Expand Up @@ -801,8 +788,8 @@ describe('NestJS orchestrion subscriber: schedule / event / bullmq', () => {
// to the factory's return (our `originalDecorator`), then the subscriber's
// `end` reassigns `data.result`. Returns effective (wrapped) decorator.
function driveFactory(channelName: string, factoryArgs: unknown[], originalDecorator: AnyFn): AnyFn {
const data: { arguments: unknown[]; result?: unknown } = { arguments: factoryArgs };
tracingChannel<{ arguments: unknown[]; result?: unknown }>(channelName).traceSync(() => originalDecorator, data);
const data: OrchestrionChannelContext = { arguments: factoryArgs };
tracingChannel<OrchestrionChannelContext>(channelName).traceSync(() => originalDecorator, data);
return data.result as AnyFn;
}

Expand Down
38 changes: 15 additions & 23 deletions packages/remix/src/server/integrations/tracing-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
waitForTracingChannelBinding,
filterCollectedUrl,
} from '@sentry/core';
import type { OrchestrionChannelContext } from '@sentry/server-utils';
import { bindTracingChannelToSpan } from '@sentry/server-utils';
import {
SENTRY_SEGMENT_NAME_SOURCE,
Expand Down Expand Up @@ -41,17 +42,6 @@ const NOOP = (): void => {};
const MATCH_ROUTE_ID = 'match.route.id';
const MATCH_PARAMS = 'match.params';

/**
* The shape orchestrion's transform attaches to a tracing-channel `context` object. Documented here
* rather than imported because orchestrion's runtime doesn't export it.
*/
interface ChannelContext {
// The live `arguments` of the wrapped call.
arguments: unknown[];
result?: unknown;
error?: unknown;
}

// `callRouteLoader`/`callRouteAction` receive a single options object as `arguments[0]`.
interface RouteCallParams {
request?: Request;
Expand All @@ -61,7 +51,7 @@ interface RouteCallParams {

// The in-flight form-data read, started at span start (before the action consumes the body) so it
// overlaps the action's execution rather than starting after it settles.
interface ActionChannelContext extends ChannelContext {
interface ActionChannelContext extends OrchestrionChannelContext {
_sentryFormData?: Promise<FormData>;
}

Expand Down Expand Up @@ -140,7 +130,7 @@ function enrichActiveSpanWithRoute(result: unknown): void {
}

function subscribeRequestHandler(): void {
bindTracingChannelToSpan<ChannelContext>(
bindTracingChannelToSpan<OrchestrionChannelContext>(
diagnosticsChannel.tracingChannel(remixChannels.REMIX_REQUEST_HANDLER),
data => {
const requestAttributes = getRequestAttributes(data.arguments[0]);
Expand Down Expand Up @@ -178,19 +168,21 @@ function subscribeRequestHandler(): void {
function subscribeMatchServerRoutes(): void {
// `matchServerRoutes` is synchronous, so only the `end` event carries a result; the rest are
// no-ops. `subscribe` types demand a handler for each channel.
diagnosticsChannel.tracingChannel<ChannelContext, ChannelContext>(remixChannels.REMIX_MATCH_SERVER_ROUTES).subscribe({
start: NOOP,
end(data) {
enrichActiveSpanWithRoute(data.result);
},
asyncStart: NOOP,
asyncEnd: NOOP,
error: NOOP,
});
diagnosticsChannel
.tracingChannel<OrchestrionChannelContext, OrchestrionChannelContext>(remixChannels.REMIX_MATCH_SERVER_ROUTES)
.subscribe({
start: NOOP,
end(data) {
enrichActiveSpanWithRoute(data.result);
},
asyncStart: NOOP,
asyncEnd: NOOP,
error: NOOP,
});
}

function subscribeCallRouteLoader(): void {
bindTracingChannelToSpan<ChannelContext>(
bindTracingChannelToSpan<OrchestrionChannelContext>(
diagnosticsChannel.tracingChannel(remixChannels.REMIX_CALL_ROUTE_LOADER),
data => {
const params = (data.arguments[0] ?? {}) as RouteCallParams;
Expand Down
5 changes: 2 additions & 3 deletions packages/remix/src/v3/assetServer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as diagnosticsChannel from 'node:diagnostics_channel';
import { consoleSandbox } from '@sentry/core';
import type { OrchestrionChannelContext } from '@sentry/server-utils';
import { remixV3Channels } from '@sentry/server-utils/orchestrion/config';
import { addDebugIdToSourceMap, findDebugId, getDebugId, injectDebugIdSnippet } from './debugId';
import { getShimUrl, isOrchestrionLoader, orchestrionLoader, resolveShimPath } from './orchestrionLoader';
Expand Down Expand Up @@ -39,9 +40,7 @@ interface AssetServer {
fetch: (request: Request) => Promise<Response | null>;
}

interface CreateAssetServerContext {
arguments: unknown[];
result?: unknown;
interface CreateAssetServerContext extends OrchestrionChannelContext {
_sentryHideSourceMaps?: boolean;
}

Expand Down
44 changes: 22 additions & 22 deletions packages/remix/src/v3/server/instrument.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as diagnosticsChannel from 'node:diagnostics_channel';
import { createMultiMatcher } from 'remix/route-pattern/match';
import { consoleSandbox } from '@sentry/core';
import type { OrchestrionChannelContext } from '@sentry/server-utils';
import { remixV3Channels } from '@sentry/server-utils/orchestrion/config';

import type { MatcherLike, RequestListenerOptionsLike, RouterOptionsLike } from '../types';
Expand All @@ -9,11 +10,6 @@ import { sentryRemixMiddleware } from './middleware';

const NOOP = (): void => {};

/** The call's arguments, as orchestrion's transform attaches them to a tracing channel context. */
interface ChannelContext {
arguments: unknown[];
}

// Marks an options object already injected into, so the same mutation cannot be applied twice.
const INJECTED = Symbol.for('SentryRemixV3Injected');

Expand Down Expand Up @@ -41,22 +37,24 @@ export function instrumentRemixV3(): void {
}

function subscribeToCreateRouter(): void {
diagnosticsChannel.tracingChannel<ChannelContext, ChannelContext>(remixV3Channels.REMIX_V3_CREATE_ROUTER).subscribe({
start(data) {
// Node rethrows anything this handler throws as an uncaught exception, which would kill an app
// that runs fine without Sentry. Frozen options, a non-writable property and a `route-pattern`
// copy that cannot build a matcher all reach here, so the router is left uninstrumented instead.
try {
injectRouterMiddleware(ensureOptions(data.arguments));
} catch {
// Ignored on purpose.
}
},
end: NOOP,
asyncStart: NOOP,
asyncEnd: NOOP,
error: NOOP,
});
diagnosticsChannel
.tracingChannel<OrchestrionChannelContext, OrchestrionChannelContext>(remixV3Channels.REMIX_V3_CREATE_ROUTER)
.subscribe({
start(data) {
// Node rethrows anything this handler throws as an uncaught exception, which would kill an app
// that runs fine without Sentry. Frozen options, a non-writable property and a `route-pattern`
// copy that cannot build a matcher all reach here, so the router is left uninstrumented instead.
try {
injectRouterMiddleware(ensureOptions(data.arguments));
} catch {
// Ignored on purpose.
}
},
end: NOOP,
asyncStart: NOOP,
asyncEnd: NOOP,
error: NOOP,
});
}

/**
Expand All @@ -66,7 +64,9 @@ function subscribeToCreateRouter(): void {
*/
function subscribeToCreateRequestListener(): void {
diagnosticsChannel
.tracingChannel<ChannelContext, ChannelContext>(remixV3Channels.REMIX_V3_CREATE_REQUEST_LISTENER)
.tracingChannel<OrchestrionChannelContext, OrchestrionChannelContext>(
remixV3Channels.REMIX_V3_CREATE_REQUEST_LISTENER,
)
.subscribe({
start(data) {
try {
Expand Down
1 change: 1 addition & 0 deletions packages/server-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { attachHapiErrorHandler as _attachHapiErrorHandler } from './integration
export { bindTracingChannelToSpan } from './tracing-channel';
export type { TracingChannelPayloadWithSpan } from './tracing-channel';
export type { InstrumentationConfig } from './orchestrion/apmTypes';
export type { OrchestrionChannelContext } from './orchestrion/types';
// Runtime target of the bundler-injected module snippet. The snippet imports it
// from this entry alongside the module's subscriber factory (see
// `orchestrion/bundler/moduleInjectedTransform.ts`); it is a plain runtime
Expand Down
Loading
Loading