@inversifyjs/http-instrumentation-core 0.0.0-stage → 5.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ # Typescript compiled files
2
+ /lib/**
3
+
4
+ /tsconfig.tsbuildinfo
5
+ /tsconfig.esm.tsbuildinfo
6
+
7
+ # Test coverage report
8
+ /coverage
9
+
10
+ # Test mutation report
11
+ /reports
12
+
13
+ # node modules
14
+ /node_modules/
15
+
16
+ # Turborepo files
17
+ .turbo/
@@ -0,0 +1,4 @@
1
+ {
2
+ "*.js": ["prettier --write"],
3
+ "*.ts": ["prettier --write", "eslint"]
4
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # @inversifyjs/http-instrumentation-core
2
+
3
+ ## 5.8.0
4
+
5
+ No changes in this release.
6
+
7
+ ## 5.7.0
8
+
9
+ ### Minor Changes
10
+
11
+ Add optional HTTP instrumentation sinks. Event types and the sink live in `@inversifyjs/http-instrumentation-core`. Adapters emit pipeline events only when at least one sink is configured. The Express 5 adapter also records native middleware, and the Express 5, Fastify, Hono, and uWebSockets adapters record request and response headers and status.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 inversify
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,6 @@
1
- # Temporary Holding Version
1
+ [![Test coverage](https://codecov.io/gh/inversify/monorepo/branch/main/graph/badge.svg?flag=%40inversifyjs%2Fhttp-instrumentation-core)](https://codecov.io/gh/inversify/monorepo/branch/main/graph/badge.svg?flag=%40inversifyjs%2Fhttp-instrumentation-core)
2
+ [![npm version](https://img.shields.io/github/package-json/v/inversify/monorepo?filename=packages%2Fframework%2Fhttp%2Flibraries%2Finstrumentation-core%2Fpackage.json&style=plastic)](https://www.npmjs.com/package/@inversifyjs/http-instrumentation-core)
2
3
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
4
+ # @inversifyjs/http-instrumentation-core
5
+
6
+ Event types, sink, and execution scope for InversifyJS HTTP instrumentation.
@@ -0,0 +1,23 @@
1
+ import { buildDefaultConfig } from '@inversifyjs/foundation-eslint-config';
2
+
3
+ import plugin from '@inversifyjs/eslint-plugin-require-extensions';
4
+
5
+ export default [
6
+ ...buildDefaultConfig(
7
+ {
8
+ '@typescript-eslint/consistent-type-imports': [
9
+ 'error',
10
+ {
11
+ disallowTypeAnnotations: true,
12
+ fixStyle: 'inline-type-imports',
13
+ prefer: 'type-imports',
14
+ },
15
+ ],
16
+ 'require-extensions/require-extensions': 'error',
17
+ 'require-extensions/require-index': 'error',
18
+ },
19
+ {
20
+ 'require-extensions': plugin,
21
+ },
22
+ ),
23
+ ];
package/package.json CHANGED
@@ -1,6 +1,67 @@
1
1
  {
2
+ "author": "Adrián Martínez Jiménez",
3
+ "bugs": {
4
+ "url": "https://github.com/inversify/monorepo/issues"
5
+ },
6
+ "description": "InversifyJS HTTP instrumentation events and sink",
7
+ "dependencies": {
8
+ "@inversifyjs/common": "2.0.1",
9
+ "@inversifyjs/framework-core": "2.2.0"
10
+ },
11
+ "devDependencies": {
12
+ "@inversifyjs/eslint-plugin-require-extensions": "0.2.0",
13
+ "@stryker-mutator/core": "10.0.0",
14
+ "@stryker-mutator/typescript-checker": "10.0.0",
15
+ "@stryker-mutator/vitest-runner": "10.0.0",
16
+ "@types/node": "24.19.1",
17
+ "@vitest/coverage-v8": "5.0.3",
18
+ "eslint": "10.12.0",
19
+ "prettier": "3.9.9",
20
+ "rimraf": "6.1.3",
21
+ "typescript": "6.0.3",
22
+ "vitest": "5.0.3"
23
+ },
24
+ "devEngines": {
25
+ "node": "^24.10.0",
26
+ "pnpm": "^12.0.0"
27
+ },
28
+ "homepage": "https://inversify.io",
29
+ "keywords": [
30
+ "dependency injection",
31
+ "dependency inversion",
32
+ "di",
33
+ "instrumentation",
34
+ "inversion of control container",
35
+ "ioc",
36
+ "javascript",
37
+ "node",
38
+ "typescript"
39
+ ],
40
+ "license": "MIT",
41
+ "exports": {
42
+ ".": "./lib/index.js"
43
+ },
2
44
  "name": "@inversifyjs/http-instrumentation-core",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/inversify/monorepo.git"
51
+ },
52
+ "version": "5.8.0",
53
+ "type": "module",
54
+ "scripts": {
55
+ "build": "tsc",
56
+ "build:clean": "rimraf lib",
57
+ "format": "prettier --write ./src",
58
+ "lint": "eslint ./src",
59
+ "prebuild": "pnpm run build:clean",
60
+ "test": "vitest run",
61
+ "test:integration": "pnpm run test --project Integration",
62
+ "test:coverage": "pnpm run test:unit --coverage",
63
+ "test:mutation": "stryker run",
64
+ "test:uncommitted": "pnpm run test --changed=HEAD",
65
+ "test:unit": "pnpm run test --project Unit"
66
+ }
6
67
  }
@@ -0,0 +1,3 @@
1
+ import config from '@inversifyjs/foundation-prettier-config';
2
+
3
+ export default config;
@@ -0,0 +1,130 @@
1
+ import {
2
+ afterAll,
3
+ beforeAll,
4
+ describe,
5
+ expect,
6
+ it,
7
+ type Mock,
8
+ vitest,
9
+ } from 'vitest';
10
+
11
+ import { type EventSink } from '../models/EventSink.js';
12
+ import { type HttpInstrumentationEvent } from '../models/HttpInstrumentationEvent.js';
13
+ import { emitHttpInstrumentationEvent } from './emitHttpInstrumentationEvent.js';
14
+
15
+ describe(emitHttpInstrumentationEvent, () => {
16
+ describe('having three sinks and a middle sink that throws', () => {
17
+ let events: HttpInstrumentationEvent[];
18
+ let reportSinkError: Mock<(error: unknown) => void>;
19
+ let eventFixture: HttpInstrumentationEvent;
20
+
21
+ beforeAll(() => {
22
+ events = [];
23
+ reportSinkError = vitest.fn<(error: unknown) => void>();
24
+ eventFixture = {
25
+ executionId: 'execution-1',
26
+ headers: {},
27
+ method: 'GET',
28
+ requestId: 'request-1',
29
+ timestamp: 1,
30
+ type: 'http.request.started',
31
+ url: '/orders',
32
+ };
33
+
34
+ const sinks: EventSink<HttpInstrumentationEvent>[] = [
35
+ {
36
+ emit: (event: HttpInstrumentationEvent): void => {
37
+ events.push(event);
38
+ },
39
+ },
40
+ {
41
+ emit: (): void => {
42
+ throw new Error('sink failed');
43
+ },
44
+ },
45
+ {
46
+ emit: (event: HttpInstrumentationEvent): void => {
47
+ events.push(event);
48
+ },
49
+ },
50
+ ];
51
+
52
+ emitHttpInstrumentationEvent(sinks, eventFixture, reportSinkError);
53
+ });
54
+
55
+ afterAll(() => {
56
+ vitest.clearAllMocks();
57
+ });
58
+
59
+ it('should deliver the event to the sinks that do not throw', () => {
60
+ expect(events).toStrictEqual([eventFixture, eventFixture]);
61
+ });
62
+
63
+ it('should freeze the event', () => {
64
+ expect(Object.isFrozen(eventFixture)).toBe(true);
65
+ });
66
+
67
+ it('should report the sink failure', () => {
68
+ expect(reportSinkError).toHaveBeenCalledExactlyOnceWith(
69
+ expect.any(Error),
70
+ );
71
+ });
72
+ });
73
+
74
+ describe('having a sink that returns a rejected promise', () => {
75
+ let events: HttpInstrumentationEvent[];
76
+ let reportSinkError: Mock<(error: unknown) => void>;
77
+ let eventFixture: HttpInstrumentationEvent;
78
+ let sinkError: Error;
79
+
80
+ beforeAll(async () => {
81
+ events = [];
82
+ reportSinkError = vitest.fn<(error: unknown) => void>();
83
+ sinkError = new Error('async sink');
84
+ eventFixture = {
85
+ executionId: 'execution-1',
86
+ headers: {},
87
+ method: 'GET',
88
+ requestId: 'request-1',
89
+ timestamp: 1,
90
+ type: 'http.request.started',
91
+ url: '/orders',
92
+ };
93
+
94
+ const fulfilledSink: {
95
+ emit: (event: HttpInstrumentationEvent) => Promise<void>;
96
+ } = {
97
+ emit: async (event: HttpInstrumentationEvent): Promise<void> => {
98
+ events.push(event);
99
+ },
100
+ };
101
+ const rejectedSink: { emit: () => Promise<void> } = {
102
+ emit: async (): Promise<void> => Promise.reject(sinkError),
103
+ };
104
+ const sinks: EventSink<HttpInstrumentationEvent>[] = [
105
+ fulfilledSink,
106
+ rejectedSink,
107
+ {
108
+ emit: (event: HttpInstrumentationEvent): void => {
109
+ events.push(event);
110
+ },
111
+ },
112
+ ];
113
+
114
+ emitHttpInstrumentationEvent(sinks, eventFixture, reportSinkError);
115
+ await Promise.resolve();
116
+ });
117
+
118
+ afterAll(() => {
119
+ vitest.clearAllMocks();
120
+ });
121
+
122
+ it('should deliver the event to the other sinks', () => {
123
+ expect(events).toStrictEqual([eventFixture, eventFixture]);
124
+ });
125
+
126
+ it('should report the rejection', () => {
127
+ expect(reportSinkError).toHaveBeenCalledExactlyOnceWith(sinkError);
128
+ });
129
+ });
130
+ });
@@ -0,0 +1,28 @@
1
+ import { isPromise } from '@inversifyjs/common';
2
+
3
+ import { type EventSink } from '../models/EventSink.js';
4
+ import { type HttpInstrumentationEvent } from '../models/HttpInstrumentationEvent.js';
5
+
6
+ export function emitHttpInstrumentationEvent(
7
+ sinks: readonly EventSink<HttpInstrumentationEvent>[],
8
+ event: HttpInstrumentationEvent,
9
+ reportSinkError: (error: unknown) => void,
10
+ ): void {
11
+ const publishedEvent: HttpInstrumentationEvent = Object.freeze(event);
12
+
13
+ for (const sink of sinks) {
14
+ try {
15
+ // EventSink.emit is typed as void, and an async implementation still type-checks.
16
+ // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
17
+ const result: unknown = sink.emit(publishedEvent);
18
+
19
+ if (isPromise(result)) {
20
+ void result.then(undefined, (error: unknown): void => {
21
+ reportSinkError(error);
22
+ });
23
+ }
24
+ } catch (error: unknown) {
25
+ reportSinkError(error);
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { type HttpInstrumentationScope } from '../models/HttpInstrumentationScope.js';
4
+ import {
5
+ closeHttpInstrumentationScope,
6
+ initializeHttpInstrumentationRequest,
7
+ openHttpInstrumentationScope,
8
+ } from './httpInstrumentationState.js';
9
+
10
+ describe(openHttpInstrumentationScope, () => {
11
+ describe('having a request with a nested execution', () => {
12
+ describe('when called', () => {
13
+ it('should parent the inner scope to the outer execution', () => {
14
+ const request: object = {};
15
+
16
+ initializeHttpInstrumentationRequest(request, 'request-1');
17
+
18
+ const outer: HttpInstrumentationScope = openHttpInstrumentationScope(
19
+ request,
20
+ 'execution-outer',
21
+ );
22
+ const inner: HttpInstrumentationScope = openHttpInstrumentationScope(
23
+ request,
24
+ 'execution-inner',
25
+ );
26
+
27
+ expect(outer).toStrictEqual({
28
+ executionId: 'execution-outer',
29
+ parentExecutionId: undefined,
30
+ requestId: 'request-1',
31
+ });
32
+ expect(inner).toStrictEqual({
33
+ executionId: 'execution-inner',
34
+ parentExecutionId: 'execution-outer',
35
+ requestId: 'request-1',
36
+ });
37
+
38
+ closeHttpInstrumentationScope(request, inner.executionId);
39
+
40
+ const sibling: HttpInstrumentationScope = openHttpInstrumentationScope(
41
+ request,
42
+ 'execution-sibling',
43
+ );
44
+
45
+ expect(sibling.parentExecutionId).toBe('execution-outer');
46
+ });
47
+ });
48
+ });
49
+ });
@@ -0,0 +1,93 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import { type HttpInstrumentationScope } from '../models/HttpInstrumentationScope.js';
4
+ import {
5
+ type HttpInstrumentationState,
6
+ httpInstrumentationStateSymbol,
7
+ } from '../models/httpInstrumentationStateSymbol.js';
8
+
9
+ interface HttpInstrumentationStateCarrier {
10
+ [httpInstrumentationStateSymbol]?: HttpInstrumentationState | undefined;
11
+ }
12
+
13
+ export function initializeHttpInstrumentationRequest(
14
+ request: object,
15
+ requestId: string,
16
+ ): void {
17
+ const carrier: HttpInstrumentationStateCarrier = request;
18
+
19
+ if (carrier[httpInstrumentationStateSymbol] !== undefined) {
20
+ return;
21
+ }
22
+
23
+ carrier[httpInstrumentationStateSymbol] = {
24
+ executionStack: [],
25
+ requestId,
26
+ };
27
+ }
28
+
29
+ export function openHttpInstrumentationScope(
30
+ request: object,
31
+ executionId: string,
32
+ ): HttpInstrumentationScope {
33
+ const state: HttpInstrumentationState =
34
+ getOrCreateHttpInstrumentationState(request);
35
+ const parentExecutionId: string | undefined = state.executionStack.at(-1);
36
+
37
+ state.executionStack.push(executionId);
38
+
39
+ return {
40
+ executionId,
41
+ parentExecutionId,
42
+ requestId: state.requestId,
43
+ };
44
+ }
45
+
46
+ export function closeHttpInstrumentationScope(
47
+ request: object,
48
+ executionId: string,
49
+ ): void {
50
+ const state: HttpInstrumentationState | undefined =
51
+ readHttpInstrumentationState(request);
52
+
53
+ if (state === undefined) {
54
+ return;
55
+ }
56
+
57
+ const executionIndex: number = state.executionStack.lastIndexOf(executionId);
58
+
59
+ if (executionIndex === -1) {
60
+ return;
61
+ }
62
+
63
+ state.executionStack.splice(executionIndex, 1);
64
+ }
65
+
66
+ function getOrCreateHttpInstrumentationState(
67
+ request: object,
68
+ ): HttpInstrumentationState {
69
+ const carrier: HttpInstrumentationStateCarrier = request;
70
+ const state: HttpInstrumentationState | undefined =
71
+ carrier[httpInstrumentationStateSymbol];
72
+
73
+ if (state !== undefined) {
74
+ return state;
75
+ }
76
+
77
+ const createdState: HttpInstrumentationState = {
78
+ executionStack: [],
79
+ requestId: randomUUID(),
80
+ };
81
+
82
+ carrier[httpInstrumentationStateSymbol] = createdState;
83
+
84
+ return createdState;
85
+ }
86
+
87
+ function readHttpInstrumentationState(
88
+ request: object,
89
+ ): HttpInstrumentationState | undefined {
90
+ return (request as HttpInstrumentationStateCarrier)[
91
+ httpInstrumentationStateSymbol
92
+ ];
93
+ }
@@ -0,0 +1,16 @@
1
+ import { httpInstrumentedHandlerSymbol } from '../models/httpInstrumentedHandlerSymbol.js';
2
+
3
+ export function markHttpInstrumentedHandler(handler: object): void {
4
+ Object.defineProperty(handler, httpInstrumentedHandlerSymbol, {
5
+ configurable: false,
6
+ enumerable: false,
7
+ value: true,
8
+ writable: false,
9
+ });
10
+ }
11
+
12
+ export function isHttpInstrumentedHandler(handler: object): boolean {
13
+ const record: Record<symbol, unknown> = handler as Record<symbol, unknown>;
14
+
15
+ return record[httpInstrumentedHandlerSymbol] === true;
16
+ }
@@ -0,0 +1,76 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import { startHttpStage } from '../calculations/startHttpStage.js';
4
+ import { type HttpInstrumentationContext } from '../models/HttpInstrumentationContext.js';
5
+ import { type HttpInstrumentationEvent } from '../models/HttpInstrumentationEvent.js';
6
+ import { type HttpInstrumentationScope } from '../models/HttpInstrumentationScope.js';
7
+ import { type HttpStageClock } from '../models/HttpStageClock.js';
8
+ import { emitHttpInstrumentationEvent } from './emitHttpInstrumentationEvent.js';
9
+ import {
10
+ closeHttpInstrumentationScope,
11
+ openHttpInstrumentationScope,
12
+ } from './httpInstrumentationState.js';
13
+
14
+ export type HttpStageOutcome<TResult> =
15
+ | {
16
+ readonly error: unknown;
17
+ }
18
+ | {
19
+ readonly result: TResult;
20
+ };
21
+
22
+ export async function runInstrumentedHttpStage<TResult>(
23
+ instrumentation: HttpInstrumentationContext,
24
+ request: object,
25
+ createStartedEvent: (
26
+ scope: HttpInstrumentationScope,
27
+ stage: HttpStageClock,
28
+ ) => HttpInstrumentationEvent,
29
+ operation: () => Promise<TResult>,
30
+ createExecutedEvent: (
31
+ scope: HttpInstrumentationScope,
32
+ stage: HttpStageClock,
33
+ outcome: HttpStageOutcome<TResult>,
34
+ ) => HttpInstrumentationEvent,
35
+ recover: (error: unknown) => Promise<TResult>,
36
+ ): Promise<TResult> {
37
+ const scope: HttpInstrumentationScope = openHttpInstrumentationScope(
38
+ request,
39
+ randomUUID(),
40
+ );
41
+ const stage: HttpStageClock = startHttpStage();
42
+
43
+ try {
44
+ emitHttpInstrumentationEvent(
45
+ instrumentation.sinks,
46
+ createStartedEvent(scope, stage),
47
+ instrumentation.reportSinkError,
48
+ );
49
+
50
+ try {
51
+ const result: TResult = await operation();
52
+
53
+ emitHttpInstrumentationEvent(
54
+ instrumentation.sinks,
55
+ createExecutedEvent(scope, stage, {
56
+ result,
57
+ }),
58
+ instrumentation.reportSinkError,
59
+ );
60
+
61
+ return result;
62
+ } catch (error: unknown) {
63
+ emitHttpInstrumentationEvent(
64
+ instrumentation.sinks,
65
+ createExecutedEvent(scope, stage, {
66
+ error,
67
+ }),
68
+ instrumentation.reportSinkError,
69
+ );
70
+
71
+ return await recover(error);
72
+ }
73
+ } finally {
74
+ closeHttpInstrumentationScope(request, scope.executionId);
75
+ }
76
+ }
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { type EventSink } from '../models/EventSink.js';
4
+ import { type HttpInstrumentationEvent } from '../models/HttpInstrumentationEvent.js';
5
+ import { buildHttpInstrumentationContext } from './buildHttpInstrumentationContext.js';
6
+
7
+ describe(buildHttpInstrumentationContext, () => {
8
+ describe('having no sinks', () => {
9
+ describe('when called', () => {
10
+ it('should leave instrumentation disabled', () => {
11
+ expect(
12
+ buildHttpInstrumentationContext(undefined, (): void => undefined),
13
+ ).toBeUndefined();
14
+ expect(
15
+ buildHttpInstrumentationContext([], (): void => undefined),
16
+ ).toBeUndefined();
17
+ });
18
+ });
19
+ });
20
+
21
+ describe('having sinks', () => {
22
+ describe('when called', () => {
23
+ it('should keep the sinks', () => {
24
+ const sink: EventSink<HttpInstrumentationEvent> = {
25
+ emit: (): void => undefined,
26
+ };
27
+
28
+ expect(
29
+ buildHttpInstrumentationContext([sink], (): void => undefined),
30
+ ).toStrictEqual({
31
+ reportSinkError: expect.any(Function),
32
+ sinks: [sink],
33
+ });
34
+ });
35
+ });
36
+ });
37
+ });
@@ -0,0 +1,17 @@
1
+ import { type EventSink } from '../models/EventSink.js';
2
+ import { type HttpInstrumentationContext } from '../models/HttpInstrumentationContext.js';
3
+ import { type HttpInstrumentationEvent } from '../models/HttpInstrumentationEvent.js';
4
+
5
+ export function buildHttpInstrumentationContext(
6
+ sinks: readonly EventSink<HttpInstrumentationEvent>[] | undefined,
7
+ reportSinkError: (error: unknown) => void,
8
+ ): HttpInstrumentationContext | undefined {
9
+ if (sinks === undefined || sinks.length === 0) {
10
+ return undefined;
11
+ }
12
+
13
+ return {
14
+ reportSinkError,
15
+ sinks,
16
+ };
17
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { redactHttpHeaders } from './redactHttpHeaders.js';
4
+
5
+ describe(redactHttpHeaders, () => {
6
+ describe('having credential headers', () => {
7
+ describe('when called', () => {
8
+ it('should redact credential headers and keep the others', () => {
9
+ const headers: Readonly<Record<string, string | readonly string[]>> =
10
+ redactHttpHeaders({
11
+ authorization: 'secret',
12
+ 'content-length': 12,
13
+ cookie: 'session=1',
14
+ 'proxy-authorization': 'basic secret',
15
+ 'set-cookie': ['a=1', 'b=2'],
16
+ 'x-api-key': 'key-1',
17
+ 'x-auth-token': 'token-1',
18
+ 'x-empty': undefined,
19
+ 'x-request-id': 'req-1',
20
+ });
21
+
22
+ expect(Object.isFrozen(headers)).toBe(true);
23
+ expect(headers).toStrictEqual({
24
+ authorization: '[redacted]',
25
+ 'content-length': '12',
26
+ cookie: '[redacted]',
27
+ 'proxy-authorization': '[redacted]',
28
+ 'set-cookie': '[redacted]',
29
+ 'x-api-key': '[redacted]',
30
+ 'x-auth-token': '[redacted]',
31
+ 'x-request-id': 'req-1',
32
+ });
33
+ });
34
+ });
35
+ });
36
+ });
@@ -0,0 +1,38 @@
1
+ const REDACTED_HEADER_VALUE: string = '[redacted]';
2
+
3
+ const REDACTED_HTTP_HEADER_NAMES: ReadonlySet<string> = new Set([
4
+ 'authorization',
5
+ 'cookie',
6
+ 'proxy-authorization',
7
+ 'set-cookie',
8
+ 'x-api-key',
9
+ 'x-auth-token',
10
+ ]);
11
+
12
+ export function redactHttpHeaders(
13
+ headers: Readonly<
14
+ Record<string, number | string | readonly string[] | undefined>
15
+ >,
16
+ ): Record<string, string | readonly string[]> {
17
+ const redactedHeaders: Record<string, string | readonly string[]> = {};
18
+
19
+ for (const key in headers) {
20
+ if (!Object.hasOwn(headers, key)) {
21
+ continue;
22
+ }
23
+
24
+ const value: number | string | readonly string[] | undefined = headers[key];
25
+
26
+ if (value === undefined) {
27
+ continue;
28
+ }
29
+
30
+ redactedHeaders[key] = REDACTED_HTTP_HEADER_NAMES.has(key.toLowerCase())
31
+ ? REDACTED_HEADER_VALUE
32
+ : typeof value === 'number'
33
+ ? value.toString()
34
+ : value;
35
+ }
36
+
37
+ return Object.freeze(redactedHeaders);
38
+ }
@@ -0,0 +1,12 @@
1
+ import { type HttpStageClock } from '../models/HttpStageClock.js';
2
+
3
+ export function startHttpStage(): HttpStageClock {
4
+ return {
5
+ startedAt: Date.now(),
6
+ startTick: performance.now(),
7
+ };
8
+ }
9
+
10
+ export function readHttpStageDuration(stage: HttpStageClock): number {
11
+ return performance.now() - stage.startTick;
12
+ }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ export { emitHttpInstrumentationEvent } from './actions/emitHttpInstrumentationEvent.js';
2
+ export {
3
+ closeHttpInstrumentationScope,
4
+ initializeHttpInstrumentationRequest,
5
+ openHttpInstrumentationScope,
6
+ } from './actions/httpInstrumentationState.js';
7
+ export {
8
+ isHttpInstrumentedHandler,
9
+ markHttpInstrumentedHandler,
10
+ } from './actions/markHttpInstrumentedHandler.js';
11
+ export {
12
+ type HttpStageOutcome,
13
+ runInstrumentedHttpStage,
14
+ } from './actions/runInstrumentedHttpStage.js';
15
+ export { buildHttpInstrumentationContext } from './calculations/buildHttpInstrumentationContext.js';
16
+ export { redactHttpHeaders } from './calculations/redactHttpHeaders.js';
17
+ export {
18
+ readHttpStageDuration,
19
+ startHttpStage,
20
+ } from './calculations/startHttpStage.js';
21
+ export { type EventSink } from './models/EventSink.js';
22
+ export { type HttpInstrumentationContext } from './models/HttpInstrumentationContext.js';
23
+ export { type HttpInstrumentationEvent } from './models/HttpInstrumentationEvent.js';
24
+ export { type HttpInstrumentationScope } from './models/HttpInstrumentationScope.js';
25
+ export { httpInstrumentedHandlerSymbol } from './models/httpInstrumentedHandlerSymbol.js';
26
+ export { type HttpStageClock } from './models/HttpStageClock.js';
@@ -0,0 +1,3 @@
1
+ export interface EventSink<TEvent> {
2
+ emit(event: TEvent): void;
3
+ }
@@ -0,0 +1,7 @@
1
+ import { type EventSink } from './EventSink.js';
2
+ import { type HttpInstrumentationEvent } from './HttpInstrumentationEvent.js';
3
+
4
+ export interface HttpInstrumentationContext {
5
+ readonly reportSinkError: (error: unknown) => void;
6
+ readonly sinks: readonly EventSink<HttpInstrumentationEvent>[];
7
+ }
@@ -0,0 +1,187 @@
1
+ import { type MiddlewarePhase } from '@inversifyjs/framework-core';
2
+
3
+ interface HttpControllerExecutedEvent {
4
+ readonly controller: string;
5
+ readonly duration: number;
6
+ readonly error?: unknown;
7
+ readonly executionId: string;
8
+ readonly method: string;
9
+ readonly parentExecutionId?: string;
10
+ readonly requestId: string;
11
+ readonly startedAt: number;
12
+ readonly timestamp: number;
13
+ readonly type: 'http.controller.executed';
14
+ }
15
+
16
+ interface HttpControllerStartedEvent {
17
+ readonly controller: string;
18
+ readonly executionId: string;
19
+ readonly method: string;
20
+ readonly parentExecutionId?: string;
21
+ readonly requestId: string;
22
+ readonly timestamp: number;
23
+ readonly type: 'http.controller.started';
24
+ }
25
+
26
+ interface HttpErrorEvent {
27
+ readonly duration: number;
28
+ readonly error: unknown;
29
+ readonly executionId: string;
30
+ readonly filter?: string;
31
+ readonly parentExecutionId?: string;
32
+ readonly requestId: string;
33
+ readonly startedAt: number;
34
+ readonly timestamp: number;
35
+ readonly type: 'http.error';
36
+ }
37
+
38
+ interface HttpGuardExecutedEvent {
39
+ readonly allowed?: boolean;
40
+ readonly duration: number;
41
+ readonly error?: unknown;
42
+ readonly executionId: string;
43
+ readonly guard: string;
44
+ readonly parentExecutionId?: string;
45
+ readonly requestId: string;
46
+ readonly startedAt: number;
47
+ readonly timestamp: number;
48
+ readonly type: 'http.guard.executed';
49
+ }
50
+
51
+ interface HttpGuardStartedEvent {
52
+ readonly executionId: string;
53
+ readonly guard: string;
54
+ readonly parentExecutionId?: string;
55
+ readonly requestId: string;
56
+ readonly timestamp: number;
57
+ readonly type: 'http.guard.started';
58
+ }
59
+
60
+ interface HttpInterceptorExecutedEvent {
61
+ readonly duration: number;
62
+ readonly error?: unknown;
63
+ readonly executionId: string;
64
+ readonly interceptor: string;
65
+ readonly parentExecutionId?: string;
66
+ readonly requestId: string;
67
+ readonly startedAt: number;
68
+ readonly timestamp: number;
69
+ readonly type: 'http.interceptor.executed';
70
+ }
71
+
72
+ interface HttpInterceptorStartedEvent {
73
+ readonly executionId: string;
74
+ readonly interceptor: string;
75
+ readonly parentExecutionId?: string;
76
+ readonly requestId: string;
77
+ readonly timestamp: number;
78
+ readonly type: 'http.interceptor.started';
79
+ }
80
+
81
+ interface HttpMiddlewareExecutedEvent {
82
+ readonly duration: number;
83
+ readonly error?: unknown;
84
+ readonly executionId: string;
85
+ readonly middleware: string;
86
+ readonly parentExecutionId?: string;
87
+ readonly phase: MiddlewarePhase;
88
+ readonly requestId: string;
89
+ readonly startedAt: number;
90
+ readonly timestamp: number;
91
+ readonly type: 'http.middleware.executed';
92
+ }
93
+
94
+ interface HttpMiddlewareStartedEvent {
95
+ readonly executionId: string;
96
+ readonly middleware: string;
97
+ readonly parentExecutionId?: string;
98
+ readonly phase: MiddlewarePhase;
99
+ readonly requestId: string;
100
+ readonly timestamp: number;
101
+ readonly type: 'http.middleware.started';
102
+ }
103
+
104
+ interface HttpNativeMiddlewareExecutedEvent {
105
+ readonly duration: number;
106
+ readonly error?: unknown;
107
+ readonly executionId: string;
108
+ readonly name: string;
109
+ readonly parentExecutionId?: string;
110
+ readonly requestId: string;
111
+ readonly startedAt: number;
112
+ readonly timestamp: number;
113
+ readonly type: 'http.nativeMiddleware.executed';
114
+ }
115
+
116
+ interface HttpNativeMiddlewareStartedEvent {
117
+ readonly executionId: string;
118
+ readonly name: string;
119
+ readonly parentExecutionId?: string;
120
+ readonly requestId: string;
121
+ readonly timestamp: number;
122
+ readonly type: 'http.nativeMiddleware.started';
123
+ }
124
+
125
+ interface HttpPipeExecutedEvent {
126
+ readonly duration: number;
127
+ readonly error?: unknown;
128
+ readonly executionId: string;
129
+ readonly method: string;
130
+ readonly parameterIndex: number;
131
+ readonly parentExecutionId?: string;
132
+ readonly pipe: string;
133
+ readonly requestId: string;
134
+ readonly startedAt: number;
135
+ readonly timestamp: number;
136
+ readonly type: 'http.pipe.executed';
137
+ }
138
+
139
+ interface HttpPipeStartedEvent {
140
+ readonly executionId: string;
141
+ readonly method: string;
142
+ readonly parameterIndex: number;
143
+ readonly parentExecutionId?: string;
144
+ readonly pipe: string;
145
+ readonly requestId: string;
146
+ readonly timestamp: number;
147
+ readonly type: 'http.pipe.started';
148
+ }
149
+
150
+ interface HttpRequestStartedEvent {
151
+ readonly executionId: string;
152
+ readonly headers: Readonly<Record<string, string | readonly string[]>>;
153
+ readonly method: string;
154
+ readonly requestId: string;
155
+ readonly timestamp: number;
156
+ readonly type: 'http.request.started';
157
+ readonly url: string;
158
+ }
159
+
160
+ interface HttpResponseSentEvent {
161
+ readonly aborted: boolean;
162
+ readonly duration: number;
163
+ readonly executionId: string;
164
+ readonly headers: Readonly<Record<string, string | readonly string[]>>;
165
+ readonly requestId: string;
166
+ readonly startedAt: number;
167
+ readonly statusCode: number;
168
+ readonly timestamp: number;
169
+ readonly type: 'http.response.sent';
170
+ }
171
+
172
+ export type HttpInstrumentationEvent =
173
+ | HttpControllerExecutedEvent
174
+ | HttpControllerStartedEvent
175
+ | HttpErrorEvent
176
+ | HttpGuardExecutedEvent
177
+ | HttpGuardStartedEvent
178
+ | HttpInterceptorExecutedEvent
179
+ | HttpInterceptorStartedEvent
180
+ | HttpMiddlewareExecutedEvent
181
+ | HttpMiddlewareStartedEvent
182
+ | HttpNativeMiddlewareExecutedEvent
183
+ | HttpNativeMiddlewareStartedEvent
184
+ | HttpPipeExecutedEvent
185
+ | HttpPipeStartedEvent
186
+ | HttpRequestStartedEvent
187
+ | HttpResponseSentEvent;
@@ -0,0 +1,5 @@
1
+ export interface HttpInstrumentationScope {
2
+ readonly executionId: string;
3
+ readonly parentExecutionId: string | undefined;
4
+ readonly requestId: string;
5
+ }
@@ -0,0 +1,4 @@
1
+ export interface HttpStageClock {
2
+ readonly startedAt: number;
3
+ readonly startTick: number;
4
+ }
@@ -0,0 +1,8 @@
1
+ export const httpInstrumentationStateSymbol: unique symbol = Symbol.for(
2
+ '@inversifyjs/http-instrumentation-core/httpInstrumentationState',
3
+ );
4
+
5
+ export interface HttpInstrumentationState {
6
+ executionStack: string[];
7
+ requestId: string;
8
+ }
@@ -0,0 +1,3 @@
1
+ export const httpInstrumentedHandlerSymbol: unique symbol = Symbol.for(
2
+ '@inversifyjs/http-instrumentation-core/httpInstrumentedHandler',
3
+ );
@@ -0,0 +1,3 @@
1
+ import config from '@inversifyjs/foundation-stryker-config';
2
+
3
+ export default config;
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "http://json.schemastore.org/tsconfig",
3
+ "extends": "@inversifyjs/foundation-typescript-config/tsconfig.base.esm.json",
4
+ "compilerOptions": {
5
+ "outDir": "./lib",
6
+ "rootDir": "./src",
7
+ "tsBuildInfoFile": "tsconfig.esm.tsbuildinfo",
8
+ "types": ["node"]
9
+ },
10
+ "include": ["src"]
11
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "http://json.schemastore.org/tsconfig",
3
+ "extends": "./tsconfig.esm.json"
4
+ }
@@ -0,0 +1,3 @@
1
+ import { defaultConfig } from '@inversifyjs/foundation-vitest-config';
2
+
3
+ export default defaultConfig;
@@ -0,0 +1,3 @@
1
+ import { strykerConfig } from '@inversifyjs/foundation-vitest-config';
2
+
3
+ export default strykerConfig;