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
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Opting in via `experimentalUseDiagnosticsChannelInjection()` (before `init`)
// is all that's needed.
//
// `Sentry.init()` swaps the OTel `Hapi` integration
// for the diagnostics-channel one and synchronously
// installs the module hooks that inject the channels.
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.experimentalUseDiagnosticsChannelInjection();

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
});
207 changes: 112 additions & 95 deletions dev-packages/node-integration-tests/suites/tracing/hapi/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@ describe('hapi auto-instrumentation', () => {
cleanupChildProcesses();
});

const EXPECTED_TRANSACTION = {
const expectedTransaction = (origin: string): Record<string, unknown> => ({
transaction: 'GET /',
spans: expect.arrayContaining([
expect.objectContaining({
data: expect.objectContaining({
'http.route': '/',
'http.method': 'GET',
'hapi.type': 'router',
'sentry.origin': 'auto.http.otel.hapi',
'sentry.origin': origin,
'sentry.op': 'router.hapi',
}),
description: 'GET /',
op: 'router.hapi',
origin: 'auto.http.otel.hapi',
origin,
status: 'ok',
}),
]),
};
});

const EXPECTED_ERROR_EVENT = {
exception: {
Expand All @@ -36,101 +36,118 @@ describe('hapi auto-instrumentation', () => {
},
};

createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
test('should auto-instrument `@hapi/hapi` package.', async () => {
const runner = createRunner().expect({ transaction: EXPECTED_TRANSACTION }).start();
runner.makeRequest('get', '/');
await runner.completed();
});
// The orchestrion (diagnostics-channel injection) path produces the same hapi
// span ops and attributes as the OTel path; only the span origin differs to
// signal the injection mechanism (`auto.http.orchestrion.hapi` vs
// `auto.http.otel.hapi`), mirroring the mysql orchestrion integration.
// `@hapi/hapi` is in the injected version range (`>=17.0.0 <22.0.0`), and the
// channels are injected synchronously by `Sentry.init()`, so no extra Node
// flags are needed and ESM works too.
const INSTRUMENT_FILES = ['instrument.mjs', 'instrument-orchestrion.mjs'] as const;

test('should instrument plugin routes and server extensions.', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'GET /plugin-route',
spans: expect.arrayContaining([
expect.objectContaining({
description: 'GET /plugin-route',
op: 'plugin.hapi',
origin: 'auto.http.otel.hapi',
data: expect.objectContaining({
'http.route': '/plugin-route',
'hapi.type': 'plugin',
'hapi.plugin.name': 'testPlugin',
'sentry.op': 'plugin.hapi',
'sentry.origin': 'auto.http.otel.hapi',
}),
}),
expect.objectContaining({
description: 'ext - onPreResponse',
op: 'server.ext.hapi',
origin: 'auto.http.otel.hapi',
data: expect.objectContaining({
'hapi.type': 'server.ext',
'server.ext.type': 'onPreResponse',
'sentry.op': 'server.ext.hapi',
'sentry.origin': 'auto.http.otel.hapi',
}),
}),
]),
},
})
.start();
runner.makeRequest('get', '/plugin-route');
await runner.completed();
});
for (const instrument of INSTRUMENT_FILES) {
const origin = instrument === 'instrument.mjs' ? 'auto.http.otel.hapi' : 'auto.http.orchestrion.hapi';

test('should handle returned plain errors in routes.', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'GET /error',
},
})
.expect({ event: EXPECTED_ERROR_EVENT })
.start();
runner.makeRequest('get', '/error', { expectError: true });
await runner.completed();
});
describe(instrument === 'instrument.mjs' ? 'opentelemetry' : 'diagnostics-channel (orchestrion)', () => {
createEsmAndCjsTests(__dirname, 'scenario.mjs', instrument, (createRunner, test) => {
test('should auto-instrument `@hapi/hapi` package.', async () => {
const runner = createRunner()
.expect({ transaction: expectedTransaction(origin) })
.start();
runner.makeRequest('get', '/');
await runner.completed();
});

test('should assign parameterized transactionName to error.', async () => {
const runner = createRunner()
.expect({
event: {
...EXPECTED_ERROR_EVENT,
transaction: 'GET /error/{id}',
},
})
.ignore('transaction')
.start();
runner.makeRequest('get', '/error/123', { expectError: true });
await runner.completed();
});
test('should instrument plugin routes and server extensions.', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'GET /plugin-route',
spans: expect.arrayContaining([
expect.objectContaining({
description: 'GET /plugin-route',
op: 'plugin.hapi',
origin,
data: expect.objectContaining({
'http.route': '/plugin-route',
'hapi.type': 'plugin',
'hapi.plugin.name': 'testPlugin',
'sentry.op': 'plugin.hapi',
'sentry.origin': origin,
}),
}),
expect.objectContaining({
description: 'ext - onPreResponse',
op: 'server.ext.hapi',
origin,
data: expect.objectContaining({
'hapi.type': 'server.ext',
'server.ext.type': 'onPreResponse',
'sentry.op': 'server.ext.hapi',
'sentry.origin': origin,
}),
}),
]),
},
})
.start();
runner.makeRequest('get', '/plugin-route');
await runner.completed();
});

test('should handle returned Boom errors in routes.', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'GET /boom-error',
},
})
.expect({ event: EXPECTED_ERROR_EVENT })
.start();
runner.makeRequest('get', '/boom-error', { expectError: true });
await runner.completed();
});
test('should handle returned plain errors in routes.', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'GET /error',
},
})
.expect({ event: EXPECTED_ERROR_EVENT })
.start();
runner.makeRequest('get', '/error', { expectError: true });
await runner.completed();
});

test('should assign parameterized transactionName to error.', async () => {
const runner = createRunner()
.expect({
event: {
...EXPECTED_ERROR_EVENT,
transaction: 'GET /error/{id}',
},
})
.ignore('transaction')
.start();
runner.makeRequest('get', '/error/123', { expectError: true });
await runner.completed();
});

test('should handle promise rejections in routes.', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'GET /promise-error',
},
})
.expect({ event: EXPECTED_ERROR_EVENT })
.start();
runner.makeRequest('get', '/promise-error', { expectError: true });
await runner.completed();
test('should handle returned Boom errors in routes.', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'GET /boom-error',
},
})
.expect({ event: EXPECTED_ERROR_EVENT })
.start();
runner.makeRequest('get', '/boom-error', { expectError: true });
await runner.completed();
});

test('should handle promise rejections in routes.', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'GET /promise-error',
},
})
.expect({ event: EXPECTED_ERROR_EVENT })
.start();
runner.makeRequest('get', '/promise-error', { expectError: true });
await runner.completed();
});
});
});
});
}
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
mysqlChannelIntegration,
lruMemoizerChannelIntegration,
hapiChannelIntegration,
detectOrchestrionSetup,
} from '@sentry/server-utils/orchestrion';
import { registerDiagnosticsChannelInjection } from '@sentry/server-utils/orchestrion/register';
Expand Down Expand Up @@ -41,7 +42,11 @@ import { setDiagnosticsChannelInjectionLoader } from './diagnosticsChannelInject
*/
export function experimentalUseDiagnosticsChannelInjection(): void {
setDiagnosticsChannelInjectionLoader((): DiagnosticsChannelInjection => {
const integrations = [mysqlChannelIntegration(), lruMemoizerChannelIntegration()] as const;
const integrations = [
mysqlChannelIntegration(),
lruMemoizerChannelIntegration(),
hapiChannelIntegration(),
] as const;
const replacedOtelIntegrationNames = integrations.map(i => i.name);

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Structural type definitions and constants ported from the vendored
* `@opentelemetry/instrumentation-hapi` types, with all `@hapi/*` and
* `@opentelemetry/*` dependencies removed. Only the shapes actually accessed by
* the orchestrion hapi subscriber are kept.
*/

// Single source of truth for the request lifecycle extension points, so the
// `ServerRequestExtType` union and the runtime `HapiLifecycleMethodNames` set
// below can't drift apart.
const LIFECYCLE_EXT_POINTS = [
'onPreAuth',
'onCredentials',
'onPostAuth',
'onPreHandler',
'onPostHandler',
'onPreResponse',
'onRequest',
] as const;

export type ServerRequestExtType = (typeof LIFECYCLE_EXT_POINTS)[number];

export type LifecycleMethod = (request: unknown, h: unknown, err?: Error) => unknown;

export interface ServerRouteOptions {
handler?: LifecycleMethod | unknown;
[key: string]: unknown;
}

export interface ServerRoute {
path: string;
method: string;
handler?: LifecycleMethod | unknown;
options?: ((server: unknown) => ServerRouteOptions) | ServerRouteOptions;
[key: string]: unknown;
}

export interface ServerExtEventsObject {
type: string;
[key: string]: unknown;
}

export interface ServerExtEventsRequestObject {
type: ServerRequestExtType;
method: LifecycleMethod;
[key: string]: unknown;
}

export interface ServerExtOptions {
[key: string]: unknown;
}

/**
* This symbol is used to mark a Hapi route handler or server extension handler as
* already patched, since it's possible to use these handlers multiple times
* i.e. when allowing multiple versions of one plugin, or when registering a plugin
* multiple times on different servers.
*/
export const handlerPatched: unique symbol = Symbol('hapi-handler-patched');

export type PatchableServerRoute = ServerRoute & {
[handlerPatched]?: boolean;
};

export type PatchableExtMethod = LifecycleMethod & {
[handlerPatched]?: boolean;
};

export type ServerExtDirectInput = [ServerRequestExtType, LifecycleMethod, (ServerExtOptions | undefined)?];

export const HapiLayerType = {
ROUTER: 'router',
PLUGIN: 'plugin',
EXT: 'server.ext',
} as const;

export const HapiLifecycleMethodNames = new Set<string>(LIFECYCLE_EXT_POINTS);

export enum AttributeNames {
HAPI_TYPE = 'hapi.type',
PLUGIN_NAME = 'hapi.plugin.name',
EXT_TYPE = 'server.ext.type',
}
Loading
Loading