qase-javascript-commons 2.5.5 → 2.5.7
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/changelog.md +14 -0
- package/dist/client/clientV2.d.ts +1 -0
- package/dist/client/clientV2.js +11 -1
- package/dist/config/config-validation-schema.d.ts +24 -0
- package/dist/config/config-validation-schema.js +22 -0
- package/dist/models/index.d.ts +1 -0
- package/dist/models/step-data.d.ts +9 -0
- package/dist/models/test-step.d.ts +4 -3
- package/dist/models/test-step.js +13 -1
- package/dist/options/options-type.d.ts +5 -0
- package/dist/profilers/abstract-profiler.d.ts +5 -0
- package/dist/profilers/abstract-profiler.js +6 -0
- package/dist/profilers/fetch-interceptor.d.ts +60 -0
- package/dist/profilers/fetch-interceptor.js +277 -0
- package/dist/profilers/http-interceptor.d.ts +69 -0
- package/dist/profilers/http-interceptor.js +245 -0
- package/dist/profilers/index.d.ts +5 -0
- package/dist/profilers/index.js +11 -0
- package/dist/profilers/network-profiler.d.ts +61 -0
- package/dist/profilers/network-profiler.js +98 -0
- package/dist/reporters/report-reporter.js +4 -0
- package/package.json +17 -2
package/changelog.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# qase-javascript-commons@2.5.7
|
|
2
|
+
|
|
3
|
+
## Bug fixes
|
|
4
|
+
|
|
5
|
+
- Moved profiler modules to a separate sub-path export (`qase-javascript-commons/profilers`) to fix Webpack `UnhandledSchemeError` when using Cypress. The main entry point no longer pulls in `node:` URI imports (`async_hooks`, `http`, `https`, `diagnostics_channel`).
|
|
6
|
+
|
|
7
|
+
# qase-javascript-commons@2.5.6
|
|
8
|
+
|
|
9
|
+
## What's new
|
|
10
|
+
|
|
11
|
+
- Added Network Profiler framework for automatic HTTP/HTTPS request interception during test execution.
|
|
12
|
+
- Added `StepType.REQUEST` step type for network request data in test results.
|
|
13
|
+
- Added configurable profiler options in `qase.config.json` (`profilers` and `networkProfiler` fields).
|
|
14
|
+
|
|
1
15
|
# qase-javascript-commons@2.5.5
|
|
2
16
|
|
|
3
17
|
## What's new
|
package/dist/client/clientV2.js
CHANGED
|
@@ -173,9 +173,12 @@ class ClientV2 extends clientV1_1.ClientV1 {
|
|
|
173
173
|
if (step.step_type === models_1.StepType.TEXT) {
|
|
174
174
|
this.processTextStep(step, resultStep, testTitle);
|
|
175
175
|
}
|
|
176
|
-
else {
|
|
176
|
+
else if (step.step_type === models_1.StepType.GHERKIN) {
|
|
177
177
|
this.processGherkinStep(step, resultStep);
|
|
178
178
|
}
|
|
179
|
+
else if (step.step_type === models_1.StepType.REQUEST) {
|
|
180
|
+
this.processRequestStep(step, resultStep);
|
|
181
|
+
}
|
|
179
182
|
if (step.steps.length > 0) {
|
|
180
183
|
resultStep.steps = await this.transformSteps(step.steps, testTitle);
|
|
181
184
|
}
|
|
@@ -215,6 +218,13 @@ class ClientV2 extends clientV1_1.ClientV1 {
|
|
|
215
218
|
const stepData = step.data;
|
|
216
219
|
resultStep.data.action = stepData.keyword;
|
|
217
220
|
}
|
|
221
|
+
processRequestStep(step, resultStep) {
|
|
222
|
+
if (!('request_method' in step.data) || !resultStep.data) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const stepData = step.data;
|
|
226
|
+
resultStep.data.action = `${stepData.request_method} ${stepData.request_url}`;
|
|
227
|
+
}
|
|
218
228
|
getExecution(exec) {
|
|
219
229
|
return {
|
|
220
230
|
status: ClientV2.statusMap[exec.status],
|
|
@@ -298,5 +298,29 @@ export declare const configValidationSchema: {
|
|
|
298
298
|
};
|
|
299
299
|
};
|
|
300
300
|
};
|
|
301
|
+
profilers: {
|
|
302
|
+
type: string;
|
|
303
|
+
items: {
|
|
304
|
+
type: string;
|
|
305
|
+
};
|
|
306
|
+
nullable: boolean;
|
|
307
|
+
};
|
|
308
|
+
networkProfiler: {
|
|
309
|
+
type: string;
|
|
310
|
+
nullable: boolean;
|
|
311
|
+
properties: {
|
|
312
|
+
skip_domains: {
|
|
313
|
+
type: string;
|
|
314
|
+
items: {
|
|
315
|
+
type: string;
|
|
316
|
+
};
|
|
317
|
+
nullable: boolean;
|
|
318
|
+
};
|
|
319
|
+
track_on_fail: {
|
|
320
|
+
type: string;
|
|
321
|
+
nullable: boolean;
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
};
|
|
301
325
|
};
|
|
302
326
|
};
|
|
@@ -282,5 +282,27 @@ exports.configValidationSchema = {
|
|
|
282
282
|
},
|
|
283
283
|
},
|
|
284
284
|
},
|
|
285
|
+
profilers: {
|
|
286
|
+
type: 'array',
|
|
287
|
+
items: {
|
|
288
|
+
type: 'string',
|
|
289
|
+
},
|
|
290
|
+
nullable: true,
|
|
291
|
+
},
|
|
292
|
+
networkProfiler: {
|
|
293
|
+
type: 'object',
|
|
294
|
+
nullable: true,
|
|
295
|
+
properties: {
|
|
296
|
+
skip_domains: {
|
|
297
|
+
type: 'array',
|
|
298
|
+
items: { type: 'string' },
|
|
299
|
+
nullable: true,
|
|
300
|
+
},
|
|
301
|
+
track_on_fail: {
|
|
302
|
+
type: 'boolean',
|
|
303
|
+
nullable: true,
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
},
|
|
285
307
|
},
|
|
286
308
|
};
|
package/dist/models/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { TestResultType } from './test-result';
|
|
|
2
2
|
export type { Relation, Suite, SuiteData, TestopsProjectMapping } from './test-result';
|
|
3
3
|
export { TestExecution, TestStatusEnum } from './test-execution';
|
|
4
4
|
export { TestStepType, StepType } from './test-step';
|
|
5
|
+
export type { StepRequestData, StepTextData, StepGherkinData } from './step-data';
|
|
5
6
|
export { StepStatusEnum } from './step-execution';
|
|
6
7
|
export type { Attachment } from './attachment';
|
|
7
8
|
export type { Report } from './report';
|
|
@@ -8,3 +8,12 @@ export interface StepGherkinData {
|
|
|
8
8
|
name: string;
|
|
9
9
|
line: number;
|
|
10
10
|
}
|
|
11
|
+
export interface StepRequestData {
|
|
12
|
+
request_method: string;
|
|
13
|
+
request_url: string;
|
|
14
|
+
request_headers: Record<string, string> | null;
|
|
15
|
+
request_body: string | null;
|
|
16
|
+
status_code: number | null;
|
|
17
|
+
response_body: string | null;
|
|
18
|
+
response_headers: Record<string, string> | null;
|
|
19
|
+
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { StepGherkinData, StepTextData } from './step-data';
|
|
1
|
+
import { StepGherkinData, StepRequestData, StepTextData } from './step-data';
|
|
2
2
|
import { StepExecution } from './step-execution';
|
|
3
3
|
import { Attachment } from './attachment';
|
|
4
4
|
export declare enum StepType {
|
|
5
5
|
TEXT = "text",
|
|
6
|
-
GHERKIN = "gherkin"
|
|
6
|
+
GHERKIN = "gherkin",
|
|
7
|
+
REQUEST = "request"
|
|
7
8
|
}
|
|
8
9
|
export declare class TestStepType {
|
|
9
10
|
id: string;
|
|
10
11
|
step_type: StepType;
|
|
11
|
-
data: StepTextData | StepGherkinData;
|
|
12
|
+
data: StepTextData | StepGherkinData | StepRequestData;
|
|
12
13
|
parent_id: string | null;
|
|
13
14
|
execution: StepExecution;
|
|
14
15
|
attachments: Attachment[];
|
package/dist/models/test-step.js
CHANGED
|
@@ -6,6 +6,7 @@ var StepType;
|
|
|
6
6
|
(function (StepType) {
|
|
7
7
|
StepType["TEXT"] = "text";
|
|
8
8
|
StepType["GHERKIN"] = "gherkin";
|
|
9
|
+
StepType["REQUEST"] = "request";
|
|
9
10
|
})(StepType || (exports.StepType = StepType = {}));
|
|
10
11
|
class TestStepType {
|
|
11
12
|
id;
|
|
@@ -29,13 +30,24 @@ class TestStepType {
|
|
|
29
30
|
data: null,
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
|
-
else {
|
|
33
|
+
else if (type === StepType.GHERKIN) {
|
|
33
34
|
this.data = {
|
|
34
35
|
keyword: '',
|
|
35
36
|
name: '',
|
|
36
37
|
line: 0,
|
|
37
38
|
};
|
|
38
39
|
}
|
|
40
|
+
else {
|
|
41
|
+
this.data = {
|
|
42
|
+
request_method: '',
|
|
43
|
+
request_url: '',
|
|
44
|
+
request_headers: null,
|
|
45
|
+
request_body: null,
|
|
46
|
+
status_code: null,
|
|
47
|
+
response_body: null,
|
|
48
|
+
response_headers: null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
39
51
|
}
|
|
40
52
|
}
|
|
41
53
|
exports.TestStepType = TestStepType;
|
|
@@ -31,6 +31,11 @@ export type OptionsType = {
|
|
|
31
31
|
/** Multi-project configuration (used when mode is testops_multi). */
|
|
32
32
|
testops_multi?: TestOpsMultiConfigType | undefined;
|
|
33
33
|
report?: RecursivePartial<AdditionalReportOptionsType> | undefined;
|
|
34
|
+
profilers?: string[] | undefined;
|
|
35
|
+
networkProfiler?: {
|
|
36
|
+
skip_domains?: string[] | undefined;
|
|
37
|
+
track_on_fail?: boolean | undefined;
|
|
38
|
+
} | undefined;
|
|
34
39
|
};
|
|
35
40
|
export type FrameworkOptionsType<F extends string, O> = {
|
|
36
41
|
framework?: Partial<Record<F, O>>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { TestStepType } from '../models/test-step';
|
|
3
|
+
interface ProfilerRef {
|
|
4
|
+
shouldSkip(url: string): boolean;
|
|
5
|
+
trackOnFail: boolean;
|
|
6
|
+
fallbackSteps: TestStepType[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* FetchInterceptor subscribes to undici diagnostics_channel events to capture
|
|
10
|
+
* outgoing fetch() calls as REQUEST-type steps in the AsyncLocalStorage store.
|
|
11
|
+
*
|
|
12
|
+
* Works with Node.js 18+ global fetch (which uses undici internally).
|
|
13
|
+
* Gracefully skips subscription on Node < 18 (no global fetch).
|
|
14
|
+
*
|
|
15
|
+
* Key design decisions:
|
|
16
|
+
* - WeakMap keyed on undici request objects (same reference across all events)
|
|
17
|
+
* - ALS context captured at undici:request:create time, NOT at headers time.
|
|
18
|
+
* This is critical because undici connection pooling can break AsyncLocalStorage
|
|
19
|
+
* propagation between the create and headers events when connections are reused.
|
|
20
|
+
* - globalThis.fetch wrapping intercepts error response bodies (undici:request:bodyChunkReceived
|
|
21
|
+
* is not available in Node 22's built-in undici)
|
|
22
|
+
* - All handlers wrapped in try/catch (INTC-06: silent failure)
|
|
23
|
+
*
|
|
24
|
+
* Event flow:
|
|
25
|
+
* undici:request:create → capture method, URL, start time, ALS context → WeakMap
|
|
26
|
+
* undici:request:headers → build step using WeakMap data, push to ALS accumulator
|
|
27
|
+
* undici:request:trailers → cleanup WeakMap entry
|
|
28
|
+
* fetch wrapper → update response_body for error responses (>= 400)
|
|
29
|
+
*/
|
|
30
|
+
export declare class FetchInterceptor {
|
|
31
|
+
private readonly store;
|
|
32
|
+
private readonly profiler;
|
|
33
|
+
private readonly pendingRequests;
|
|
34
|
+
private readonly bodyHandlers;
|
|
35
|
+
private readonly createHandler;
|
|
36
|
+
private readonly headersHandler;
|
|
37
|
+
private readonly trailersHandler;
|
|
38
|
+
private origFetch;
|
|
39
|
+
constructor(store: AsyncLocalStorage<TestStepType[]>, profiler: ProfilerRef);
|
|
40
|
+
/**
|
|
41
|
+
* Subscribe to undici diagnostics_channel events and wrap global fetch for
|
|
42
|
+
* error response body capture.
|
|
43
|
+
*
|
|
44
|
+
* No-op on Node < 18 (no global fetch) or if diagnostics_channel unavailable.
|
|
45
|
+
*/
|
|
46
|
+
subscribe(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Unsubscribe from diagnostics_channel events and restore original global fetch.
|
|
49
|
+
*/
|
|
50
|
+
unsubscribe(): void;
|
|
51
|
+
private handleCreate;
|
|
52
|
+
private handleHeaders;
|
|
53
|
+
private handleTrailers;
|
|
54
|
+
/**
|
|
55
|
+
* Called by the fetch wrapper when an error response body is available.
|
|
56
|
+
* Finds the most recently added step for this URL/status and updates its response_body.
|
|
57
|
+
*/
|
|
58
|
+
private applyResponseBody;
|
|
59
|
+
}
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FetchInterceptor = void 0;
|
|
4
|
+
const http_interceptor_1 = require("./http-interceptor");
|
|
5
|
+
// ─── FetchInterceptor ─────────────────────────────────────────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* FetchInterceptor subscribes to undici diagnostics_channel events to capture
|
|
8
|
+
* outgoing fetch() calls as REQUEST-type steps in the AsyncLocalStorage store.
|
|
9
|
+
*
|
|
10
|
+
* Works with Node.js 18+ global fetch (which uses undici internally).
|
|
11
|
+
* Gracefully skips subscription on Node < 18 (no global fetch).
|
|
12
|
+
*
|
|
13
|
+
* Key design decisions:
|
|
14
|
+
* - WeakMap keyed on undici request objects (same reference across all events)
|
|
15
|
+
* - ALS context captured at undici:request:create time, NOT at headers time.
|
|
16
|
+
* This is critical because undici connection pooling can break AsyncLocalStorage
|
|
17
|
+
* propagation between the create and headers events when connections are reused.
|
|
18
|
+
* - globalThis.fetch wrapping intercepts error response bodies (undici:request:bodyChunkReceived
|
|
19
|
+
* is not available in Node 22's built-in undici)
|
|
20
|
+
* - All handlers wrapped in try/catch (INTC-06: silent failure)
|
|
21
|
+
*
|
|
22
|
+
* Event flow:
|
|
23
|
+
* undici:request:create → capture method, URL, start time, ALS context → WeakMap
|
|
24
|
+
* undici:request:headers → build step using WeakMap data, push to ALS accumulator
|
|
25
|
+
* undici:request:trailers → cleanup WeakMap entry
|
|
26
|
+
* fetch wrapper → update response_body for error responses (>= 400)
|
|
27
|
+
*/
|
|
28
|
+
class FetchInterceptor {
|
|
29
|
+
store;
|
|
30
|
+
profiler;
|
|
31
|
+
// WeakMap keyed on undici request objects (same reference across create/headers/trailers events)
|
|
32
|
+
pendingRequests = new WeakMap();
|
|
33
|
+
// Map from step to body-update callback (for error response body capture)
|
|
34
|
+
bodyHandlers = new WeakMap();
|
|
35
|
+
// Bound handler references (needed for subscribe/unsubscribe identity)
|
|
36
|
+
createHandler;
|
|
37
|
+
headersHandler;
|
|
38
|
+
trailersHandler;
|
|
39
|
+
// Saved original fetch for wrapping/unwrapping
|
|
40
|
+
origFetch = null;
|
|
41
|
+
constructor(store, profiler) {
|
|
42
|
+
this.store = store;
|
|
43
|
+
this.profiler = profiler;
|
|
44
|
+
// Bind handlers as arrow functions for subscribe/unsubscribe reference identity
|
|
45
|
+
this.createHandler = (msg) => {
|
|
46
|
+
try {
|
|
47
|
+
this.handleCreate(msg);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// INTC-06: silent failure
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
this.headersHandler = (msg) => {
|
|
54
|
+
try {
|
|
55
|
+
this.handleHeaders(msg);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// INTC-06: silent failure
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
this.trailersHandler = (msg) => {
|
|
62
|
+
try {
|
|
63
|
+
this.handleTrailers(msg);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// INTC-06: silent failure
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Subscribe to undici diagnostics_channel events and wrap global fetch for
|
|
72
|
+
* error response body capture.
|
|
73
|
+
*
|
|
74
|
+
* No-op on Node < 18 (no global fetch) or if diagnostics_channel unavailable.
|
|
75
|
+
*/
|
|
76
|
+
subscribe() {
|
|
77
|
+
// Guard: require global fetch (Node 18+)
|
|
78
|
+
if (typeof globalThis.fetch !== 'function') {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
let dc;
|
|
82
|
+
try {
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
84
|
+
dc = require('node:diagnostics_channel');
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return; // diagnostics_channel unavailable — skip entirely
|
|
88
|
+
}
|
|
89
|
+
if (typeof dc.subscribe !== 'function') {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
dc.subscribe('undici:request:create', this.createHandler);
|
|
93
|
+
dc.subscribe('undici:request:headers', this.headersHandler);
|
|
94
|
+
dc.subscribe('undici:request:trailers', this.trailersHandler);
|
|
95
|
+
// Wrap global fetch to intercept error response bodies
|
|
96
|
+
// (undici:request:bodyChunkReceived not available in Node 22 built-in undici)
|
|
97
|
+
this.origFetch = globalThis.fetch;
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
99
|
+
const self = this;
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
101
|
+
globalThis.fetch = async function wrappedFetch(input, init) {
|
|
102
|
+
const origFetch = self.origFetch;
|
|
103
|
+
if (!origFetch) {
|
|
104
|
+
throw new Error('fetch not available');
|
|
105
|
+
}
|
|
106
|
+
const response = await origFetch.call(globalThis, input, init);
|
|
107
|
+
// Intercept error responses to capture body
|
|
108
|
+
if (response.status >= 400 && self.profiler.trackOnFail) {
|
|
109
|
+
try {
|
|
110
|
+
// Clone to read body without consuming the original response
|
|
111
|
+
const cloned = response.clone();
|
|
112
|
+
void cloned.text().then((bodyText) => {
|
|
113
|
+
try {
|
|
114
|
+
self.applyResponseBody(input, response.status, bodyText);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// INTC-06: silent failure
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// INTC-06: silent failure
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return response;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Unsubscribe from diagnostics_channel events and restore original global fetch.
|
|
130
|
+
*/
|
|
131
|
+
unsubscribe() {
|
|
132
|
+
// Restore original fetch
|
|
133
|
+
if (this.origFetch !== null) {
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
135
|
+
globalThis.fetch = this.origFetch;
|
|
136
|
+
this.origFetch = null;
|
|
137
|
+
}
|
|
138
|
+
let dc;
|
|
139
|
+
try {
|
|
140
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
141
|
+
dc = require('node:diagnostics_channel');
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// Feature-detect unsubscribe (Pitfall 5: not available in all Node versions)
|
|
147
|
+
if (typeof dc.unsubscribe !== 'function') {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
dc.unsubscribe('undici:request:create', this.createHandler);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// silent
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
dc.unsubscribe('undici:request:headers', this.headersHandler);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// silent
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
dc.unsubscribe('undici:request:trailers', this.trailersHandler);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// silent
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// ─── Private event handlers ─────────────────────────────────────────────────
|
|
170
|
+
handleCreate(msg) {
|
|
171
|
+
const message = msg;
|
|
172
|
+
const req = message.request;
|
|
173
|
+
if (!req)
|
|
174
|
+
return;
|
|
175
|
+
// CRITICAL: Capture ALS context HERE (at create time), not in headersHandler.
|
|
176
|
+
// Due to undici connection pooling, store.getStore() may return undefined in
|
|
177
|
+
// the headers event when connections are reused across test runs.
|
|
178
|
+
const acc = this.store.getStore();
|
|
179
|
+
// acc may be null/undefined when outside a run() context — use fallback accumulator in that case
|
|
180
|
+
const method = typeof req['method'] === 'string' ? req['method'] : 'GET';
|
|
181
|
+
const origin = typeof req['origin'] === 'string' ? req['origin'] : '';
|
|
182
|
+
const path = typeof req['path'] === 'string' ? req['path'] : '/';
|
|
183
|
+
const url = `${origin}${path}`;
|
|
184
|
+
// Check shouldSkip
|
|
185
|
+
if (this.profiler.shouldSkip(url)) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.pendingRequests.set(req, {
|
|
189
|
+
method,
|
|
190
|
+
url,
|
|
191
|
+
startTime: Date.now(),
|
|
192
|
+
alsContext: acc ?? null,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
handleHeaders(msg) {
|
|
196
|
+
const message = msg;
|
|
197
|
+
const req = message.request;
|
|
198
|
+
if (!req)
|
|
199
|
+
return;
|
|
200
|
+
const pending = this.pendingRequests.get(req);
|
|
201
|
+
if (!pending)
|
|
202
|
+
return;
|
|
203
|
+
// Use the ALS context captured at create time — do NOT call store.getStore() here.
|
|
204
|
+
// Connection pooling in undici can break ALS propagation between events.
|
|
205
|
+
const acc = pending.alsContext;
|
|
206
|
+
const endTime = Date.now();
|
|
207
|
+
const statusCode = message.response?.statusCode ?? 0;
|
|
208
|
+
const step = (0, http_interceptor_1.buildRequestStep)({
|
|
209
|
+
method: pending.method,
|
|
210
|
+
url: pending.url,
|
|
211
|
+
statusCode,
|
|
212
|
+
responseBody: null, // filled in later for errors via fetch wrapper
|
|
213
|
+
responseHeaders: null,
|
|
214
|
+
startTime: pending.startTime,
|
|
215
|
+
endTime,
|
|
216
|
+
});
|
|
217
|
+
if (acc !== null) {
|
|
218
|
+
acc.push(step);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// No ALS context — push to fallback accumulator (for Jest/Vitest/WDIO workers)
|
|
222
|
+
this.profiler.fallbackSteps.push(step);
|
|
223
|
+
}
|
|
224
|
+
// Register body handler for error responses
|
|
225
|
+
if (statusCode >= 400 && this.profiler.trackOnFail) {
|
|
226
|
+
this.bodyHandlers.set(step, (bodyText) => {
|
|
227
|
+
step.data.response_body = bodyText;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
handleTrailers(msg) {
|
|
232
|
+
const message = msg;
|
|
233
|
+
const req = message.request;
|
|
234
|
+
if (!req)
|
|
235
|
+
return;
|
|
236
|
+
// Clean up pending state
|
|
237
|
+
this.pendingRequests.delete(req);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Called by the fetch wrapper when an error response body is available.
|
|
241
|
+
* Finds the most recently added step for this URL/status and updates its response_body.
|
|
242
|
+
*/
|
|
243
|
+
applyResponseBody(input, statusCode, bodyText) {
|
|
244
|
+
const url = typeof input === 'string'
|
|
245
|
+
? input
|
|
246
|
+
: input instanceof URL
|
|
247
|
+
? input.toString()
|
|
248
|
+
: input.url;
|
|
249
|
+
// Get all accumulated steps — use ALS context if available, fallback accumulator otherwise
|
|
250
|
+
const searchArray = this.store.getStore() ?? this.profiler.fallbackSteps;
|
|
251
|
+
if (searchArray.length === 0)
|
|
252
|
+
return;
|
|
253
|
+
// Find the last step that matches this response and has null body
|
|
254
|
+
for (let i = searchArray.length - 1; i >= 0; i--) {
|
|
255
|
+
const step = searchArray[i];
|
|
256
|
+
if (!step)
|
|
257
|
+
continue;
|
|
258
|
+
const data = step.data;
|
|
259
|
+
if (data.status_code !== statusCode)
|
|
260
|
+
continue;
|
|
261
|
+
if (data.response_body !== null)
|
|
262
|
+
continue;
|
|
263
|
+
// Check if URL matches (handle query strings, trailing slashes)
|
|
264
|
+
const stepUrl = data.request_url ?? '';
|
|
265
|
+
const inputUrlBase = url.split('?')[0] ?? url;
|
|
266
|
+
if (url.includes(stepUrl) || stepUrl.includes(inputUrlBase) || stepUrl === inputUrlBase) {
|
|
267
|
+
const handler = this.bodyHandlers.get(step);
|
|
268
|
+
if (handler) {
|
|
269
|
+
handler(bodyText);
|
|
270
|
+
this.bodyHandlers.delete(step);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
exports.FetchInterceptor = FetchInterceptor;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
import { TestStepType } from '../models/test-step';
|
|
4
|
+
interface ProfilerRef {
|
|
5
|
+
shouldSkip(url: string): boolean;
|
|
6
|
+
trackOnFail: boolean;
|
|
7
|
+
fallbackSteps: TestStepType[];
|
|
8
|
+
}
|
|
9
|
+
export interface RequestInfo {
|
|
10
|
+
method: string;
|
|
11
|
+
url: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Handles the 3 `http.request()` call signatures:
|
|
15
|
+
* - (url: string, options?, callback?)
|
|
16
|
+
* - (url: URL, options?, callback?)
|
|
17
|
+
* - (options: RequestOptions, callback?)
|
|
18
|
+
*
|
|
19
|
+
* Returns `{ method, url }`. Default method is 'GET'.
|
|
20
|
+
*/
|
|
21
|
+
export declare function extractRequestInfo(args: [
|
|
22
|
+
string | URL | http.RequestOptions,
|
|
23
|
+
(http.RequestOptions | ((res: http.IncomingMessage) => void))?,
|
|
24
|
+
((res: http.IncomingMessage) => void)?
|
|
25
|
+
]): RequestInfo;
|
|
26
|
+
/**
|
|
27
|
+
* Converts `http.IncomingHttpHeaders` (which has `string | string[] | undefined` values)
|
|
28
|
+
* to `Record<string, string>`. Multi-value headers joined with `', '`. Undefined values skipped.
|
|
29
|
+
*/
|
|
30
|
+
export declare function headersToRecord(headers: http.IncomingHttpHeaders): Record<string, string>;
|
|
31
|
+
export declare function buildRequestStep(params: {
|
|
32
|
+
method: string;
|
|
33
|
+
url: string;
|
|
34
|
+
statusCode: number;
|
|
35
|
+
responseBody: string | null;
|
|
36
|
+
responseHeaders: Record<string, string> | null;
|
|
37
|
+
startTime: number;
|
|
38
|
+
endTime: number;
|
|
39
|
+
}): TestStepType;
|
|
40
|
+
/**
|
|
41
|
+
* HttpInterceptor monkey-patches Node.js http/https module functions to capture
|
|
42
|
+
* outgoing requests as REQUEST-type steps in the AsyncLocalStorage store.
|
|
43
|
+
*
|
|
44
|
+
* Pattern:
|
|
45
|
+
* 1. `install()` — saves originals, replaces with wrappers
|
|
46
|
+
* 2. `uninstall()` — restores originals
|
|
47
|
+
*/
|
|
48
|
+
export declare class HttpInterceptor {
|
|
49
|
+
private readonly store;
|
|
50
|
+
private readonly profiler;
|
|
51
|
+
private origHttpRequest;
|
|
52
|
+
private origHttpGet;
|
|
53
|
+
private origHttpsRequest;
|
|
54
|
+
private origHttpsGet;
|
|
55
|
+
constructor(store: AsyncLocalStorage<TestStepType[]>, profiler: ProfilerRef);
|
|
56
|
+
install(): void;
|
|
57
|
+
uninstall(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Creates a wrapper around a http.request-like function.
|
|
60
|
+
* The wrapper:
|
|
61
|
+
* 1. Calls the original function to get the ClientRequest
|
|
62
|
+
* 2. Checks if there's an active ALS context — if not, returns unchanged
|
|
63
|
+
* 3. Checks shouldSkip — if true, returns unchanged
|
|
64
|
+
* 4. Intercepts the 'response' event via req.emit override
|
|
65
|
+
* 5. Builds a TestStepType on response end and pushes to accumulator
|
|
66
|
+
*/
|
|
67
|
+
private wrapRequestFn;
|
|
68
|
+
}
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.HttpInterceptor = void 0;
|
|
7
|
+
exports.extractRequestInfo = extractRequestInfo;
|
|
8
|
+
exports.headersToRecord = headersToRecord;
|
|
9
|
+
exports.buildRequestStep = buildRequestStep;
|
|
10
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
11
|
+
const node_https_1 = __importDefault(require("node:https"));
|
|
12
|
+
const uuid_1 = require("uuid");
|
|
13
|
+
const test_step_1 = require("../models/test-step");
|
|
14
|
+
const step_execution_1 = require("../models/step-execution");
|
|
15
|
+
/**
|
|
16
|
+
* Handles the 3 `http.request()` call signatures:
|
|
17
|
+
* - (url: string, options?, callback?)
|
|
18
|
+
* - (url: URL, options?, callback?)
|
|
19
|
+
* - (options: RequestOptions, callback?)
|
|
20
|
+
*
|
|
21
|
+
* Returns `{ method, url }`. Default method is 'GET'.
|
|
22
|
+
*/
|
|
23
|
+
function extractRequestInfo(args) {
|
|
24
|
+
const first = args[0];
|
|
25
|
+
if (typeof first === 'string') {
|
|
26
|
+
// Signature: (url: string, options?, callback?)
|
|
27
|
+
const options = args[1];
|
|
28
|
+
const method = options && typeof options === 'object' && !('emit' in options) && 'method' in options && typeof options.method === 'string'
|
|
29
|
+
? options.method
|
|
30
|
+
: 'GET';
|
|
31
|
+
return { method, url: first };
|
|
32
|
+
}
|
|
33
|
+
if (first instanceof URL) {
|
|
34
|
+
// Signature: (url: URL, options?, callback?)
|
|
35
|
+
const options = args[1];
|
|
36
|
+
const method = options && typeof options === 'object' && !('emit' in options) && 'method' in options && typeof options.method === 'string'
|
|
37
|
+
? options.method
|
|
38
|
+
: 'GET';
|
|
39
|
+
return { method, url: first.toString() };
|
|
40
|
+
}
|
|
41
|
+
// Signature: (options: RequestOptions, callback?)
|
|
42
|
+
const opts = first;
|
|
43
|
+
const method = opts.method ?? 'GET';
|
|
44
|
+
const protocol = opts.protocol ?? 'http:';
|
|
45
|
+
const hostname = opts.hostname ?? opts.host ?? 'localhost';
|
|
46
|
+
const port = opts.port !== undefined ? `:${String(opts.port)}` : '';
|
|
47
|
+
const path = opts.path ?? '/';
|
|
48
|
+
const url = `${protocol}//${hostname}${port}${path}`;
|
|
49
|
+
return { method, url };
|
|
50
|
+
}
|
|
51
|
+
// ─── Utility: headersToRecord ─────────────────────────────────────────────────
|
|
52
|
+
/**
|
|
53
|
+
* Converts `http.IncomingHttpHeaders` (which has `string | string[] | undefined` values)
|
|
54
|
+
* to `Record<string, string>`. Multi-value headers joined with `', '`. Undefined values skipped.
|
|
55
|
+
*/
|
|
56
|
+
function headersToRecord(headers) {
|
|
57
|
+
const result = {};
|
|
58
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
59
|
+
if (value === undefined)
|
|
60
|
+
continue;
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
result[key] = value.join(', ');
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
result[key] = value;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
// ─── Utility: buildRequestStep ───────────────────────────────────────────────
|
|
71
|
+
function buildRequestStep(params) {
|
|
72
|
+
const step = new test_step_1.TestStepType(test_step_1.StepType.REQUEST);
|
|
73
|
+
step.id = (0, uuid_1.v4)();
|
|
74
|
+
const data = step.data;
|
|
75
|
+
data.request_method = params.method;
|
|
76
|
+
data.request_url = params.url;
|
|
77
|
+
data.request_headers = null;
|
|
78
|
+
data.request_body = null;
|
|
79
|
+
data.status_code = params.statusCode;
|
|
80
|
+
data.response_body = params.responseBody;
|
|
81
|
+
data.response_headers = params.responseHeaders;
|
|
82
|
+
step.execution.start_time = params.startTime;
|
|
83
|
+
step.execution.end_time = params.endTime;
|
|
84
|
+
step.execution.duration = params.endTime - params.startTime;
|
|
85
|
+
step.execution.status = params.statusCode >= 400 ? step_execution_1.StepStatusEnum.failed : step_execution_1.StepStatusEnum.passed;
|
|
86
|
+
return step;
|
|
87
|
+
}
|
|
88
|
+
// ─── HttpInterceptor ─────────────────────────────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* HttpInterceptor monkey-patches Node.js http/https module functions to capture
|
|
91
|
+
* outgoing requests as REQUEST-type steps in the AsyncLocalStorage store.
|
|
92
|
+
*
|
|
93
|
+
* Pattern:
|
|
94
|
+
* 1. `install()` — saves originals, replaces with wrappers
|
|
95
|
+
* 2. `uninstall()` — restores originals
|
|
96
|
+
*/
|
|
97
|
+
class HttpInterceptor {
|
|
98
|
+
store;
|
|
99
|
+
profiler;
|
|
100
|
+
origHttpRequest = null;
|
|
101
|
+
origHttpGet = null;
|
|
102
|
+
origHttpsRequest = null;
|
|
103
|
+
origHttpsGet = null;
|
|
104
|
+
constructor(store, profiler) {
|
|
105
|
+
this.store = store;
|
|
106
|
+
this.profiler = profiler;
|
|
107
|
+
}
|
|
108
|
+
install() {
|
|
109
|
+
this.origHttpRequest = node_http_1.default.request;
|
|
110
|
+
this.origHttpGet = node_http_1.default.get;
|
|
111
|
+
this.origHttpsRequest = node_https_1.default.request;
|
|
112
|
+
this.origHttpsGet = node_https_1.default.get;
|
|
113
|
+
const wrappedHttpRequest = this.wrapRequestFn(this.origHttpRequest);
|
|
114
|
+
const wrappedHttpGet = this.wrapRequestFn(this.origHttpGet);
|
|
115
|
+
const wrappedHttpsRequest = this.wrapRequestFn(this.origHttpsRequest);
|
|
116
|
+
const wrappedHttpsGet = this.wrapRequestFn(this.origHttpsGet);
|
|
117
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
118
|
+
node_http_1.default.request = wrappedHttpRequest;
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
120
|
+
node_http_1.default.get = wrappedHttpGet;
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
122
|
+
node_https_1.default.request = wrappedHttpsRequest;
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
124
|
+
node_https_1.default.get = wrappedHttpsGet;
|
|
125
|
+
}
|
|
126
|
+
uninstall() {
|
|
127
|
+
if (this.origHttpRequest !== null) {
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
129
|
+
node_http_1.default.request = this.origHttpRequest;
|
|
130
|
+
this.origHttpRequest = null;
|
|
131
|
+
}
|
|
132
|
+
if (this.origHttpGet !== null) {
|
|
133
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
134
|
+
node_http_1.default.get = this.origHttpGet;
|
|
135
|
+
this.origHttpGet = null;
|
|
136
|
+
}
|
|
137
|
+
if (this.origHttpsRequest !== null) {
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
139
|
+
node_https_1.default.request = this.origHttpsRequest;
|
|
140
|
+
this.origHttpsRequest = null;
|
|
141
|
+
}
|
|
142
|
+
if (this.origHttpsGet !== null) {
|
|
143
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
144
|
+
node_https_1.default.get = this.origHttpsGet;
|
|
145
|
+
this.origHttpsGet = null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Creates a wrapper around a http.request-like function.
|
|
150
|
+
* The wrapper:
|
|
151
|
+
* 1. Calls the original function to get the ClientRequest
|
|
152
|
+
* 2. Checks if there's an active ALS context — if not, returns unchanged
|
|
153
|
+
* 3. Checks shouldSkip — if true, returns unchanged
|
|
154
|
+
* 4. Intercepts the 'response' event via req.emit override
|
|
155
|
+
* 5. Builds a TestStepType on response end and pushes to accumulator
|
|
156
|
+
*/
|
|
157
|
+
wrapRequestFn(
|
|
158
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
159
|
+
origFn) {
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
161
|
+
const self = this;
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
163
|
+
return function (...args) {
|
|
164
|
+
// Call original first to get the ClientRequest
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
166
|
+
const req = origFn.apply(this, args);
|
|
167
|
+
// Extract request info
|
|
168
|
+
let reqInfo;
|
|
169
|
+
try {
|
|
170
|
+
reqInfo = extractRequestInfo(args);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return req;
|
|
174
|
+
}
|
|
175
|
+
// Check if this domain should be skipped
|
|
176
|
+
if (self.profiler.shouldSkip(reqInfo.url)) {
|
|
177
|
+
return req;
|
|
178
|
+
}
|
|
179
|
+
const startTime = Date.now();
|
|
180
|
+
const origEmit = req.emit.bind(req);
|
|
181
|
+
// Override req.emit to intercept the 'response' event
|
|
182
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
183
|
+
req.emit = function (event, ...emitArgs) {
|
|
184
|
+
if (event === 'response') {
|
|
185
|
+
const res = emitArgs[0];
|
|
186
|
+
try {
|
|
187
|
+
const chunks = [];
|
|
188
|
+
const isError = (res.statusCode ?? 0) >= 400;
|
|
189
|
+
// Register data listener BEFORE calling original emit (Pitfall 2: stream consumption race)
|
|
190
|
+
if (isError && self.profiler.trackOnFail) {
|
|
191
|
+
res.on('data', (chunk) => {
|
|
192
|
+
try {
|
|
193
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// silent failure
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
res.on('end', () => {
|
|
201
|
+
try {
|
|
202
|
+
const endTime = Date.now();
|
|
203
|
+
const statusCode = res.statusCode ?? 0;
|
|
204
|
+
const captureBody = statusCode >= 400 && self.profiler.trackOnFail;
|
|
205
|
+
const responseBody = captureBody ? Buffer.concat(chunks).toString('utf8') : null;
|
|
206
|
+
const responseHeaders = statusCode >= 400 ? headersToRecord(res.headers) : null;
|
|
207
|
+
const step = buildRequestStep({
|
|
208
|
+
method: reqInfo.method,
|
|
209
|
+
url: reqInfo.url,
|
|
210
|
+
statusCode,
|
|
211
|
+
responseBody,
|
|
212
|
+
responseHeaders,
|
|
213
|
+
startTime,
|
|
214
|
+
endTime,
|
|
215
|
+
});
|
|
216
|
+
// Push to accumulator — re-fetch in case context is still active
|
|
217
|
+
const acc = self.store.getStore();
|
|
218
|
+
if (acc !== undefined && acc !== null) {
|
|
219
|
+
acc.push(step);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
// No ALS context — push to fallback accumulator (for Jest/Vitest/WDIO workers)
|
|
223
|
+
self.profiler.fallbackSteps.push(step);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// INTC-06: silent failure — do not propagate to test
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// INTC-06: silent failure in setup — do not propagate
|
|
233
|
+
}
|
|
234
|
+
// CRITICAL: Call original emit LAST (after registering data listeners)
|
|
235
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
236
|
+
return origEmit(event, ...emitArgs);
|
|
237
|
+
}
|
|
238
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
239
|
+
return origEmit(event, ...emitArgs);
|
|
240
|
+
};
|
|
241
|
+
return req;
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
exports.HttpInterceptor = HttpInterceptor;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { AbstractProfiler } from './abstract-profiler';
|
|
2
|
+
export { NetworkProfiler } from './network-profiler';
|
|
3
|
+
export type { NetworkProfilerOptions } from './network-profiler';
|
|
4
|
+
export { HttpInterceptor } from './http-interceptor';
|
|
5
|
+
export { FetchInterceptor } from './fetch-interceptor';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FetchInterceptor = exports.HttpInterceptor = exports.NetworkProfiler = exports.AbstractProfiler = void 0;
|
|
4
|
+
var abstract_profiler_1 = require("./abstract-profiler");
|
|
5
|
+
Object.defineProperty(exports, "AbstractProfiler", { enumerable: true, get: function () { return abstract_profiler_1.AbstractProfiler; } });
|
|
6
|
+
var network_profiler_1 = require("./network-profiler");
|
|
7
|
+
Object.defineProperty(exports, "NetworkProfiler", { enumerable: true, get: function () { return network_profiler_1.NetworkProfiler; } });
|
|
8
|
+
var http_interceptor_1 = require("./http-interceptor");
|
|
9
|
+
Object.defineProperty(exports, "HttpInterceptor", { enumerable: true, get: function () { return http_interceptor_1.HttpInterceptor; } });
|
|
10
|
+
var fetch_interceptor_1 = require("./fetch-interceptor");
|
|
11
|
+
Object.defineProperty(exports, "FetchInterceptor", { enumerable: true, get: function () { return fetch_interceptor_1.FetchInterceptor; } });
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { AbstractProfiler } from './abstract-profiler';
|
|
2
|
+
import { TestStepType } from '../models';
|
|
3
|
+
export interface NetworkProfilerOptions {
|
|
4
|
+
skipDomains?: string[] | undefined;
|
|
5
|
+
trackOnFail?: boolean | undefined;
|
|
6
|
+
}
|
|
7
|
+
export declare class NetworkProfiler extends AbstractProfiler {
|
|
8
|
+
private readonly skipDomains;
|
|
9
|
+
private readonly _trackOnFail;
|
|
10
|
+
/**
|
|
11
|
+
* Fallback accumulator for steps captured outside any run() context.
|
|
12
|
+
* Used by frameworks where test bodies cannot be wrapped in run() (Jest, Vitest, WDIO).
|
|
13
|
+
* Public so interceptors can push to it via ProfilerRef.
|
|
14
|
+
*/
|
|
15
|
+
readonly fallbackSteps: TestStepType[];
|
|
16
|
+
private httpInterceptor;
|
|
17
|
+
private fetchInterceptor;
|
|
18
|
+
constructor(options?: NetworkProfilerOptions);
|
|
19
|
+
get trackOnFail(): boolean;
|
|
20
|
+
shouldSkip(url: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Install HTTP/HTTPS monkey-patches and subscribe to undici diagnostics_channel
|
|
23
|
+
* so that outgoing requests inside a `run()` context are captured as REQUEST steps.
|
|
24
|
+
*/
|
|
25
|
+
enable(): void;
|
|
26
|
+
/**
|
|
27
|
+
* No-op per v1.3 design decision (flag-based activation model).
|
|
28
|
+
* Satisfies the AbstractProfiler contract.
|
|
29
|
+
*/
|
|
30
|
+
disable(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Uninstall HTTP/HTTPS monkey-patches and unsubscribe from undici diagnostics_channel —
|
|
33
|
+
* permanently removes interception.
|
|
34
|
+
*/
|
|
35
|
+
restore(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Wraps `fn` in an AsyncLocalStorage context so that any HTTP requests
|
|
38
|
+
* made inside `fn` are attributed to the step accumulator for this call.
|
|
39
|
+
*
|
|
40
|
+
* Returns `{ result, steps }` where `result` is the return value of `fn`
|
|
41
|
+
* and `steps` is the array of REQUEST steps captured during execution.
|
|
42
|
+
*/
|
|
43
|
+
run<T>(fn: () => Promise<T>): Promise<{
|
|
44
|
+
result: T;
|
|
45
|
+
steps: TestStepType[];
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Returns the current ALS store when called inside a `run()` context.
|
|
49
|
+
* Returns an empty array when called outside (no active context).
|
|
50
|
+
*/
|
|
51
|
+
getSteps(): TestStepType[];
|
|
52
|
+
/**
|
|
53
|
+
* Returns a copy of steps accumulated outside any run() context (fallback accumulator).
|
|
54
|
+
* Used by frameworks where test bodies cannot be wrapped in run() (Jest, Vitest, WDIO).
|
|
55
|
+
*/
|
|
56
|
+
getAllSteps(): TestStepType[];
|
|
57
|
+
/**
|
|
58
|
+
* Clears the fallback step accumulator. Call after collecting steps per test.
|
|
59
|
+
*/
|
|
60
|
+
clearSteps(): void;
|
|
61
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NetworkProfiler = void 0;
|
|
4
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
5
|
+
const abstract_profiler_1 = require("./abstract-profiler");
|
|
6
|
+
const http_interceptor_1 = require("./http-interceptor");
|
|
7
|
+
const fetch_interceptor_1 = require("./fetch-interceptor");
|
|
8
|
+
const QASE_API_HOST = 'qase.io';
|
|
9
|
+
// Module-level ALS store — shared across all NetworkProfiler instances to
|
|
10
|
+
// allow nested run() calls to see the same context.
|
|
11
|
+
const store = new node_async_hooks_1.AsyncLocalStorage();
|
|
12
|
+
class NetworkProfiler extends abstract_profiler_1.AbstractProfiler {
|
|
13
|
+
skipDomains;
|
|
14
|
+
// Stored for Phase 13 use (controls whether to capture network steps on test failure)
|
|
15
|
+
_trackOnFail;
|
|
16
|
+
/**
|
|
17
|
+
* Fallback accumulator for steps captured outside any run() context.
|
|
18
|
+
* Used by frameworks where test bodies cannot be wrapped in run() (Jest, Vitest, WDIO).
|
|
19
|
+
* Public so interceptors can push to it via ProfilerRef.
|
|
20
|
+
*/
|
|
21
|
+
fallbackSteps = [];
|
|
22
|
+
httpInterceptor = null;
|
|
23
|
+
fetchInterceptor = null;
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
super();
|
|
26
|
+
this.skipDomains = options.skipDomains ?? [];
|
|
27
|
+
this._trackOnFail = options.trackOnFail ?? true;
|
|
28
|
+
}
|
|
29
|
+
get trackOnFail() {
|
|
30
|
+
return this._trackOnFail;
|
|
31
|
+
}
|
|
32
|
+
shouldSkip(url) {
|
|
33
|
+
if (url.includes(QASE_API_HOST)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return this.skipDomains.some((domain) => url.includes(domain));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Install HTTP/HTTPS monkey-patches and subscribe to undici diagnostics_channel
|
|
40
|
+
* so that outgoing requests inside a `run()` context are captured as REQUEST steps.
|
|
41
|
+
*/
|
|
42
|
+
enable() {
|
|
43
|
+
this.httpInterceptor = new http_interceptor_1.HttpInterceptor(store, this);
|
|
44
|
+
this.httpInterceptor.install();
|
|
45
|
+
this.fetchInterceptor = new fetch_interceptor_1.FetchInterceptor(store, this);
|
|
46
|
+
this.fetchInterceptor.subscribe();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* No-op per v1.3 design decision (flag-based activation model).
|
|
50
|
+
* Satisfies the AbstractProfiler contract.
|
|
51
|
+
*/
|
|
52
|
+
disable() {
|
|
53
|
+
// Intentional no-op
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Uninstall HTTP/HTTPS monkey-patches and unsubscribe from undici diagnostics_channel —
|
|
57
|
+
* permanently removes interception.
|
|
58
|
+
*/
|
|
59
|
+
restore() {
|
|
60
|
+
this.fetchInterceptor?.unsubscribe();
|
|
61
|
+
this.fetchInterceptor = null;
|
|
62
|
+
this.httpInterceptor?.uninstall();
|
|
63
|
+
this.httpInterceptor = null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Wraps `fn` in an AsyncLocalStorage context so that any HTTP requests
|
|
67
|
+
* made inside `fn` are attributed to the step accumulator for this call.
|
|
68
|
+
*
|
|
69
|
+
* Returns `{ result, steps }` where `result` is the return value of `fn`
|
|
70
|
+
* and `steps` is the array of REQUEST steps captured during execution.
|
|
71
|
+
*/
|
|
72
|
+
async run(fn) {
|
|
73
|
+
const accumulator = [];
|
|
74
|
+
const result = await store.run(accumulator, fn);
|
|
75
|
+
return { result, steps: accumulator };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Returns the current ALS store when called inside a `run()` context.
|
|
79
|
+
* Returns an empty array when called outside (no active context).
|
|
80
|
+
*/
|
|
81
|
+
getSteps() {
|
|
82
|
+
return store.getStore() ?? [];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Returns a copy of steps accumulated outside any run() context (fallback accumulator).
|
|
86
|
+
* Used by frameworks where test bodies cannot be wrapped in run() (Jest, Vitest, WDIO).
|
|
87
|
+
*/
|
|
88
|
+
getAllSteps() {
|
|
89
|
+
return [...this.fallbackSteps];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Clears the fallback step accumulator. Call after collecting steps per test.
|
|
93
|
+
*/
|
|
94
|
+
clearSteps() {
|
|
95
|
+
this.fallbackSteps.length = 0;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.NetworkProfiler = NetworkProfiler;
|
|
@@ -279,6 +279,10 @@ class ReportReporter extends abstract_reporter_1.AbstractReporter {
|
|
|
279
279
|
input_data: null, // JS GherkinData has no data field
|
|
280
280
|
};
|
|
281
281
|
}
|
|
282
|
+
// For request steps, pass raw fields through (all 7 StepRequestData fields are preserved)
|
|
283
|
+
if (step.step_type === models_1.StepType.REQUEST && 'request_method' in data) {
|
|
284
|
+
return data;
|
|
285
|
+
}
|
|
282
286
|
return data;
|
|
283
287
|
}
|
|
284
288
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qase-javascript-commons",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.7",
|
|
4
4
|
"description": "Qase JS Reporters",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./profilers": {
|
|
13
|
+
"types": "./dist/profilers/index.d.ts",
|
|
14
|
+
"default": "./dist/profilers/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"typesVersions": {
|
|
19
|
+
"*": {
|
|
20
|
+
"profilers": ["./dist/profilers/index.d.ts"]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
7
23
|
"homepage": "https://github.com/qase-tms/qase-javascript",
|
|
8
24
|
"bugs": {
|
|
9
25
|
"url": "https://github.com/qase-tms/qase-javascript/issues"
|
|
@@ -46,7 +62,6 @@
|
|
|
46
62
|
"@types/lodash.mergewith": "^4.6.9",
|
|
47
63
|
"@types/mime-types": "^2.1.4",
|
|
48
64
|
"@types/minimatch": "^6.0.0",
|
|
49
|
-
"@types/node": "^20.19.25",
|
|
50
65
|
"@types/uuid": "^9.0.8",
|
|
51
66
|
"axios": "^1.13.5",
|
|
52
67
|
"jest": "^29.7.0",
|