@elisra-devops/docgen-data-provider 1.140.0 → 1.142.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/.github/workflows/ci.yml +2 -0
- package/bin/helpers/requestContext.d.ts +51 -0
- package/bin/helpers/requestContext.js +218 -0
- package/bin/helpers/requestContext.js.map +1 -0
- package/bin/helpers/tfs.d.ts +5 -0
- package/bin/helpers/tfs.js +24 -24
- package/bin/helpers/tfs.js.map +1 -1
- package/bin/modules/GitDataProvider.js +3 -2
- package/bin/modules/GitDataProvider.js.map +1 -1
- package/bin/modules/JfrogDataProvider.js +3 -2
- package/bin/modules/JfrogDataProvider.js.map +1 -1
- package/bin/modules/PipelinesDataProvider.js +5 -4
- package/bin/modules/PipelinesDataProvider.js.map +1 -1
- package/bin/modules/ResultDataProvider.js +32 -25
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/modules/TestDataProvider.js +4 -3
- package/bin/modules/TestDataProvider.js.map +1 -1
- package/bin/modules/TicketsDataProvider.js +26 -22
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/helpers/requestContext.test.d.ts +1 -0
- package/bin/tests/helpers/requestContext.test.js +165 -0
- package/bin/tests/helpers/requestContext.test.js.map +1 -0
- package/bin/tests/helpers/tfs.test.js +54 -0
- package/bin/tests/helpers/tfs.test.js.map +1 -1
- package/bin/tests/modules/JfrogDataProvider.test.js +4 -2
- package/bin/tests/modules/JfrogDataProvider.test.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +9 -3
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/bin/tests/modules/gitDataProvider.test.js +1 -1
- package/bin/tests/modules/gitDataProvider.test.js.map +1 -1
- package/bin/tests/modules/pipelineDataProvider.test.js +1 -1
- package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -1
- package/bin/tests/modules/testDataProvider.test.js +1 -1
- package/bin/tests/modules/testDataProvider.test.js.map +1 -1
- package/bin/tests/modules/ticketsDataProvider.test.js +2 -2
- package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -1
- package/bin/tests/utils/logger.test.d.ts +1 -0
- package/bin/tests/utils/logger.test.js +332 -0
- package/bin/tests/utils/logger.test.js.map +1 -0
- package/bin/tests/utils/runContext.test.d.ts +1 -0
- package/bin/tests/utils/runContext.test.js +51 -0
- package/bin/tests/utils/runContext.test.js.map +1 -0
- package/bin/utils/logSink.d.ts +39 -0
- package/bin/utils/logSink.js +28 -0
- package/bin/utils/logSink.js.map +1 -0
- package/bin/utils/logger.d.ts +7 -0
- package/bin/utils/logger.js +266 -27
- package/bin/utils/logger.js.map +1 -1
- package/bin/utils/runContext.d.ts +8 -0
- package/bin/utils/runContext.js +12 -0
- package/bin/utils/runContext.js.map +1 -0
- package/eslint.config.mjs +40 -0
- package/package.json +8 -3
- package/src/helpers/requestContext.ts +222 -0
- package/src/helpers/tfs.ts +30 -24
- package/src/modules/GitDataProvider.ts +3 -2
- package/src/modules/JfrogDataProvider.ts +3 -2
- package/src/modules/PipelinesDataProvider.ts +5 -4
- package/src/modules/ResultDataProvider.ts +31 -25
- package/src/modules/TestDataProvider.ts +4 -3
- package/src/modules/TicketsDataProvider.ts +28 -26
- package/src/tests/helpers/requestContext.test.ts +202 -0
- package/src/tests/helpers/tfs.test.ts +86 -0
- package/src/tests/modules/JfrogDataProvider.test.ts +5 -2
- package/src/tests/modules/ResultDataProvider.test.ts +10 -3
- package/src/tests/modules/gitDataProvider.test.ts +2 -1
- package/src/tests/modules/pipelineDataProvider.test.ts +2 -1
- package/src/tests/modules/testDataProvider.test.ts +4 -1
- package/src/tests/modules/ticketsDataProvider.test.ts +2 -2
- package/src/tests/utils/logger.test.ts +381 -0
- package/src/tests/utils/runContext.test.ts +48 -0
- package/src/utils/logSink.ts +59 -0
- package/src/utils/logger.ts +269 -29
- package/src/utils/runContext.ts +25 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { redactValue } from '../utils/logger';
|
|
2
|
+
import { CONTEXT_LIMITS } from '../utils/logSink';
|
|
3
|
+
|
|
4
|
+
// What a failed Azure DevOps request looks like to whoever is triaging it. Built once, at the
|
|
5
|
+
// TFSServices choke point, and attached to the thrown error — so every caller that logs that
|
|
6
|
+
// error (however it words its own message) carries the same sanitized request description
|
|
7
|
+
// into the log record and the diagnostics store, instead of only the call sites that happened
|
|
8
|
+
// to print the URL themselves.
|
|
9
|
+
export interface AdoRequestContext {
|
|
10
|
+
method?: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
status?: number;
|
|
13
|
+
attempt?: number;
|
|
14
|
+
requestBody?: string;
|
|
15
|
+
responseExcerpt?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const MAX_URL_LEN = CONTEXT_LIMITS.url;
|
|
19
|
+
const MAX_BODY_LEN = CONTEXT_LIMITS.requestBody;
|
|
20
|
+
const MAX_STRING_VALUE_LEN = 500;
|
|
21
|
+
const MAX_ARRAY_ITEMS = 50;
|
|
22
|
+
const MAX_PATCH_OPS = 20;
|
|
23
|
+
const MAX_RESPONSE_LEN = CONTEXT_LIMITS.responseExcerpt;
|
|
24
|
+
const MAX_TEXT_RESPONSE_LEN = 200;
|
|
25
|
+
|
|
26
|
+
// Exact names, not a substring match — "key" must not mask a legitimate "keywords" parameter.
|
|
27
|
+
const SENSITIVE_PARAM = /^(access_?token|token|pat|password|pwd|secret|sig|signature|api_?key|key|authorization|auth)$/i;
|
|
28
|
+
|
|
29
|
+
const truncate = (text: string, max: number): string => (text.length > max ? `${text.slice(0, max)}…` : text);
|
|
30
|
+
|
|
31
|
+
/** Strips credentials embedded in the URL and masks sensitive query parameter values. */
|
|
32
|
+
export function sanitizeUrl(raw: unknown): string | undefined {
|
|
33
|
+
if (typeof raw !== 'string' || !raw) return undefined;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = new URL(raw);
|
|
36
|
+
parsed.username = '';
|
|
37
|
+
parsed.password = '';
|
|
38
|
+
// Rewrite only the sensitive values in the raw query string — re-serializing through
|
|
39
|
+
// URLSearchParams would re-encode every other parameter ($depth, %20, +).
|
|
40
|
+
if (parsed.search) {
|
|
41
|
+
parsed.search = parsed.search
|
|
42
|
+
.slice(1)
|
|
43
|
+
.split('&')
|
|
44
|
+
.map((pair) => {
|
|
45
|
+
const eq = pair.indexOf('=');
|
|
46
|
+
if (eq < 0) return pair;
|
|
47
|
+
let name = pair.slice(0, eq);
|
|
48
|
+
try {
|
|
49
|
+
name = decodeURIComponent(name);
|
|
50
|
+
} catch {
|
|
51
|
+
// keep the raw name
|
|
52
|
+
}
|
|
53
|
+
return SENSITIVE_PARAM.test(name) ? `${pair.slice(0, eq)}=[REDACTED]` : pair;
|
|
54
|
+
})
|
|
55
|
+
.join('&');
|
|
56
|
+
}
|
|
57
|
+
return truncate(parsed.toString(), MAX_URL_LEN);
|
|
58
|
+
} catch {
|
|
59
|
+
return truncate(raw, MAX_URL_LEN);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Replaces each single-quoted WIQL literal with its length — the query structure (fields,
|
|
65
|
+
* operators, numbers, @macros) stays readable, free-text values never reach the logs.
|
|
66
|
+
* `''` is WIQL's escaped quote.
|
|
67
|
+
*/
|
|
68
|
+
export function maskWiqlLiterals(text: string): string {
|
|
69
|
+
return text.replace(/'(?:[^']|'')*'/g, (m) => `'[${m.length - 2} chars]'`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isBinaryLike(data: unknown): boolean {
|
|
73
|
+
return (
|
|
74
|
+
(typeof Buffer !== 'undefined' && Buffer.isBuffer(data)) ||
|
|
75
|
+
data instanceof ArrayBuffer ||
|
|
76
|
+
ArrayBuffer.isView(data as ArrayBufferView)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isJsonPatch(data: unknown): data is Array<{ op: string; path: string }> {
|
|
81
|
+
return (
|
|
82
|
+
Array.isArray(data) &&
|
|
83
|
+
data.length > 0 &&
|
|
84
|
+
data.every(
|
|
85
|
+
(item) =>
|
|
86
|
+
item &&
|
|
87
|
+
typeof item === 'object' &&
|
|
88
|
+
typeof (item as Record<string, unknown>).op === 'string' &&
|
|
89
|
+
typeof (item as Record<string, unknown>).path === 'string'
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function clampValues(value: unknown, depth = 0): unknown {
|
|
95
|
+
if (typeof value === 'string') return truncate(value, MAX_STRING_VALUE_LEN);
|
|
96
|
+
if (depth > 6 || value === null || typeof value !== 'object') return value;
|
|
97
|
+
if (Array.isArray(value)) {
|
|
98
|
+
const items = value.slice(0, MAX_ARRAY_ITEMS).map((v) => clampValues(v, depth + 1));
|
|
99
|
+
return value.length > MAX_ARRAY_ITEMS ? [...items, `…(+${value.length - MAX_ARRAY_ITEMS} more)`] : items;
|
|
100
|
+
}
|
|
101
|
+
const out: Record<string, unknown> = {};
|
|
102
|
+
for (const key of Object.keys(value as Record<string, unknown>)) {
|
|
103
|
+
out[key] = clampValues((value as Record<string, unknown>)[key], depth + 1);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A short, safe description of a request body — enough to see *what was asked* (the WIQL
|
|
110
|
+
* text, the id list) without ever carrying credentials or work-item content:
|
|
111
|
+
* - GET/HEAD never have one.
|
|
112
|
+
* - JSON-Patch arrays (work item / test case updates) keep only `op` + `path`; the `value`s
|
|
113
|
+
* are real field content and are dropped.
|
|
114
|
+
* - a WIQL `query` keeps its structure but its quoted literals are masked.
|
|
115
|
+
* - other JSON goes through redactValue (credential-looking keys scrubbed at any depth),
|
|
116
|
+
* with every string and the total length bounded.
|
|
117
|
+
* - anything that isn't plain JSON is described by type and size only.
|
|
118
|
+
*/
|
|
119
|
+
export function summarizeBody(data: unknown, method?: string): string | undefined {
|
|
120
|
+
const verb = String(method || '').toUpperCase();
|
|
121
|
+
if (verb === 'GET' || verb === 'HEAD') return undefined;
|
|
122
|
+
if (data === undefined || data === null || data === '') return undefined;
|
|
123
|
+
|
|
124
|
+
if (typeof data === 'string') {
|
|
125
|
+
try {
|
|
126
|
+
const parsed = JSON.parse(data);
|
|
127
|
+
return typeof parsed === 'string' ? `[text, ${data.length} chars]` : summarizeBody(parsed, method);
|
|
128
|
+
} catch {
|
|
129
|
+
return `[text, ${data.length} chars]`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (typeof data !== 'object') return `[${typeof data}]`;
|
|
133
|
+
if (isBinaryLike(data)) {
|
|
134
|
+
const size = (data as { byteLength?: number }).byteLength;
|
|
135
|
+
return `[binary${typeof size === 'number' ? `, ${size} bytes` : ''}]`;
|
|
136
|
+
}
|
|
137
|
+
if (typeof (data as { pipe?: unknown }).pipe === 'function') return '[stream]';
|
|
138
|
+
if (typeof FormData !== 'undefined' && data instanceof FormData) return '[form-data]';
|
|
139
|
+
|
|
140
|
+
if (isJsonPatch(data)) {
|
|
141
|
+
const ops = data.slice(0, MAX_PATCH_OPS).map((o) => `${o.op} ${o.path}`);
|
|
142
|
+
const more = data.length > MAX_PATCH_OPS ? ` …(+${data.length - MAX_PATCH_OPS} more)` : '';
|
|
143
|
+
return truncate(`json-patch (${data.length} ops, values omitted): ${ops.join('; ')}${more}`, MAX_BODY_LEN);
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(data) ? data.length === 0 : Object.keys(data).length === 0) return undefined;
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const body =
|
|
149
|
+
!Array.isArray(data) && typeof (data as { query?: unknown }).query === 'string'
|
|
150
|
+
? { ...(data as Record<string, unknown>), query: maskWiqlLiterals((data as { query: string }).query) }
|
|
151
|
+
: data;
|
|
152
|
+
return truncate(JSON.stringify(clampValues(redactValue(body))), MAX_BODY_LEN);
|
|
153
|
+
} catch {
|
|
154
|
+
return '[unserializable body]';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The server's own explanation of the failure (ADO puts it in `message`), bounded. */
|
|
159
|
+
export function summarizeResponse(data: unknown): string | undefined {
|
|
160
|
+
if (data === undefined || data === null) return undefined;
|
|
161
|
+
if (typeof data === 'string') return truncate(data, MAX_TEXT_RESPONSE_LEN);
|
|
162
|
+
if (isBinaryLike(data)) return undefined;
|
|
163
|
+
if (typeof (data as { pipe?: unknown }).pipe === 'function') return '[stream]';
|
|
164
|
+
try {
|
|
165
|
+
const message = (data as { message?: unknown }).message;
|
|
166
|
+
if (typeof message === 'string') return truncate(message, MAX_RESPONSE_LEN);
|
|
167
|
+
return truncate(JSON.stringify(redactValue(data)), MAX_RESPONSE_LEN);
|
|
168
|
+
} catch {
|
|
169
|
+
return '[unserializable response data]';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function buildAdoRequestContext(
|
|
174
|
+
error: any,
|
|
175
|
+
request: { method?: string; url?: string; data?: unknown },
|
|
176
|
+
attempt?: number
|
|
177
|
+
): AdoRequestContext {
|
|
178
|
+
const method = String(request.method || error?.config?.method || 'get').toUpperCase();
|
|
179
|
+
const context: AdoRequestContext = { method, url: sanitizeUrl(request.url ?? error?.config?.url) };
|
|
180
|
+
const status = Number(error?.response?.status);
|
|
181
|
+
if (Number.isFinite(status) && status > 0) context.status = status;
|
|
182
|
+
if (attempt !== undefined) context.attempt = attempt;
|
|
183
|
+
const requestBody = summarizeBody(request.data, method);
|
|
184
|
+
if (requestBody) context.requestBody = requestBody;
|
|
185
|
+
const responseExcerpt = summarizeResponse(error?.response?.data);
|
|
186
|
+
if (responseExcerpt) context.responseExcerpt = responseExcerpt;
|
|
187
|
+
return context;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Attaches the context as an enumerable own property: winston's errors() / log() merge an
|
|
192
|
+
* Error's own enumerable properties onto the log record, which is how the transport and the
|
|
193
|
+
* text formatter find it without every call site having to pass it along.
|
|
194
|
+
*/
|
|
195
|
+
export function annotateAdoError<T>(error: T, context: AdoRequestContext): T {
|
|
196
|
+
if (error && typeof error === 'object') {
|
|
197
|
+
try {
|
|
198
|
+
Object.defineProperty(error, 'adoRequest', { value: context, enumerable: true, configurable: true, writable: true });
|
|
199
|
+
} catch {
|
|
200
|
+
// A frozen/exotic error object just goes unannotated — never worth failing the request path.
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return error;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The log metadata for a caught error: message, stack, code, and the request description when
|
|
208
|
+
* the error came through TFSServices. Deliberately NOT the error object itself — passing a
|
|
209
|
+
* whole AxiosError would merge its entire config/request/response graph onto the record.
|
|
210
|
+
* winston concatenates meta.message onto the call's own message, so
|
|
211
|
+
* `logger.error('Could not fetch X:', describeError(err))` reads the same as the old
|
|
212
|
+
* `logger.error(\`Could not fetch X: ${err.message}\`)` — plus the request, which a message
|
|
213
|
+
* string can't carry.
|
|
214
|
+
*/
|
|
215
|
+
export function describeError(error: unknown): Record<string, unknown> {
|
|
216
|
+
if (!error || typeof error !== 'object') return { message: String(error) };
|
|
217
|
+
const e = error as Record<string, unknown>;
|
|
218
|
+
const meta: Record<string, unknown> = { message: e.message, stack: e.stack };
|
|
219
|
+
if (typeof e.code === 'string') meta.code = e.code;
|
|
220
|
+
if (e.adoRequest) meta.adoRequest = e.adoRequest;
|
|
221
|
+
return meta;
|
|
222
|
+
}
|
package/src/helpers/tfs.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
|
2
2
|
import logger from '../utils/logger';
|
|
3
|
+
import { AdoRequestContext, annotateAdoError, buildAdoRequestContext } from './requestContext';
|
|
3
4
|
|
|
4
5
|
// Environment detection
|
|
5
6
|
const isNode = typeof window === 'undefined' && typeof process !== 'undefined' && process.versions?.node;
|
|
@@ -83,7 +84,7 @@ export class TFSServices {
|
|
|
83
84
|
return res;
|
|
84
85
|
} catch (e) {
|
|
85
86
|
logger.error(`error download zip file , url : ${url}`);
|
|
86
|
-
throw new Error(String(e));
|
|
87
|
+
throw annotateAdoError(new Error(String(e)), buildAdoRequestContext(e, { method: 'GET', url }));
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
@@ -178,7 +179,7 @@ export class TFSServices {
|
|
|
178
179
|
const result = await this.axiosInstance.request(config);
|
|
179
180
|
return result.data;
|
|
180
181
|
} catch (e: any) {
|
|
181
|
-
this.
|
|
182
|
+
this.reportFailure(e, { method: 'GET', url });
|
|
182
183
|
throw e;
|
|
183
184
|
}
|
|
184
185
|
}
|
|
@@ -203,7 +204,7 @@ export class TFSServices {
|
|
|
203
204
|
const result = await this.axiosInstance.request(config);
|
|
204
205
|
return result;
|
|
205
206
|
} catch (e: any) {
|
|
206
|
-
this.
|
|
207
|
+
this.reportFailure(e, { method: requestMethod, url, data });
|
|
207
208
|
throw e;
|
|
208
209
|
}
|
|
209
210
|
}
|
|
@@ -246,10 +247,9 @@ export class TFSServices {
|
|
|
246
247
|
continue;
|
|
247
248
|
}
|
|
248
249
|
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
250
|
+
// Always annotate (so any caller that logs this error — however it words its own
|
|
251
|
+
// message — carries the request), log the dedicated record only when asked to.
|
|
252
|
+
this.reportFailure(e, { method: config.method, url, data: config.data }, attempts, printError);
|
|
253
253
|
|
|
254
254
|
throw e;
|
|
255
255
|
}
|
|
@@ -284,27 +284,33 @@ export class TFSServices {
|
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
/**
|
|
287
|
-
*
|
|
287
|
+
* Describes the failed request once, attaches it to the error, and (optionally) emits the one
|
|
288
|
+
* dedicated "ADO request failed" record.
|
|
288
289
|
*/
|
|
289
|
-
private static
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
error.response.data.message || JSON.stringify(error.response.data).substring(0, 200);
|
|
300
|
-
logger.error(`Response: ${dataMessage}`);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
} else {
|
|
304
|
-
logger.error(`Error for ${url} - ${error.message}`);
|
|
290
|
+
private static reportFailure(
|
|
291
|
+
error: any,
|
|
292
|
+
request: { method?: string; url: string; data?: unknown },
|
|
293
|
+
attempt?: number,
|
|
294
|
+
print: boolean = true
|
|
295
|
+
): void {
|
|
296
|
+
const context = buildAdoRequestContext(error, request, attempt);
|
|
297
|
+
annotateAdoError(error, context);
|
|
298
|
+
if (print) {
|
|
299
|
+
this.logDetailedError(error, context);
|
|
305
300
|
}
|
|
306
301
|
}
|
|
307
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Log detailed error information
|
|
305
|
+
*/
|
|
306
|
+
private static logDetailedError(error: any, context: AdoRequestContext): void {
|
|
307
|
+
logger.error('ADO request failed', {
|
|
308
|
+
message: error?.message,
|
|
309
|
+
stack: error?.stack,
|
|
310
|
+
adoRequest: context,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
308
314
|
private static getErrorMessage(error: any): string {
|
|
309
315
|
if (error.response?.data?.message) {
|
|
310
316
|
return JSON.stringify(error.response.data.message);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TFSServices } from '../helpers/tfs';
|
|
2
2
|
import TicketsDataProvider from './TicketsDataProvider';
|
|
3
3
|
import logger from '../utils/logger';
|
|
4
|
+
import { describeError } from '../helpers/requestContext';
|
|
4
5
|
import {
|
|
5
6
|
createLinkedRelation,
|
|
6
7
|
createRequirementRelation,
|
|
@@ -124,7 +125,7 @@ export default class GitDataProvider {
|
|
|
124
125
|
}
|
|
125
126
|
return undefined;
|
|
126
127
|
} catch (err: any) {
|
|
127
|
-
logger.warn(`File ${fileName} could not be read
|
|
128
|
+
logger.warn(`File ${fileName} could not be read:`, describeError(err));
|
|
128
129
|
return undefined;
|
|
129
130
|
}
|
|
130
131
|
}
|
|
@@ -1119,7 +1120,7 @@ export default class GitDataProvider {
|
|
|
1119
1120
|
submodules.push(subModule);
|
|
1120
1121
|
}
|
|
1121
1122
|
} catch (error: any) {
|
|
1122
|
-
logger.error(
|
|
1123
|
+
logger.error('Error in getSubmodulesData:', describeError(error));
|
|
1123
1124
|
} finally {
|
|
1124
1125
|
return submodules;
|
|
1125
1126
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TFSServices } from '../helpers/tfs';
|
|
2
2
|
|
|
3
3
|
import logger from '../utils/logger';
|
|
4
|
+
import { describeError } from '../helpers/requestContext';
|
|
4
5
|
|
|
5
6
|
export default class JfrogDataProvider {
|
|
6
7
|
orgUrl: string = '';
|
|
@@ -19,7 +20,7 @@ export default class JfrogDataProvider {
|
|
|
19
20
|
if (!serviceConnectionResponse?.url) {
|
|
20
21
|
throw new Error(`Service connection ${connectionId} returned no URL — identity may lack service-endpoint read permission`);
|
|
21
22
|
}
|
|
22
|
-
logger.debug(`service connection url ${
|
|
23
|
+
logger.debug(`service connection url ${serviceConnectionResponse.url}`);
|
|
23
24
|
return serviceConnectionResponse.url;
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -56,7 +57,7 @@ export default class JfrogDataProvider {
|
|
|
56
57
|
logger.debug(`CI Url from JFROG: ${getCiResponse.buildInfo.url}`);
|
|
57
58
|
return getCiResponse.buildInfo.url;
|
|
58
59
|
} catch (err: any) {
|
|
59
|
-
logger.error(
|
|
60
|
+
logger.error('Error occurred during querying JFrog using:', describeError(err));
|
|
60
61
|
throw err;
|
|
61
62
|
}
|
|
62
63
|
}
|
|
@@ -2,6 +2,7 @@ import { PipelineRun, Repository, ResourceRepository } from '../models/tfs-data'
|
|
|
2
2
|
import { TFSServices } from '../helpers/tfs';
|
|
3
3
|
|
|
4
4
|
import logger from '../utils/logger';
|
|
5
|
+
import { describeError } from '../helpers/requestContext';
|
|
5
6
|
import GitDataProvider from './GitDataProvider';
|
|
6
7
|
const pLimit = require('p-limit');
|
|
7
8
|
const MAX_DISCOVERY_PAGES = 50;
|
|
@@ -1252,7 +1253,7 @@ export default class PipelinesDataProvider {
|
|
|
1252
1253
|
},
|
|
1253
1254
|
parameters: parameter, //'{"Test":"123"}'
|
|
1254
1255
|
};
|
|
1255
|
-
logger.info(
|
|
1256
|
+
logger.info('Triggering build', data);
|
|
1256
1257
|
let url = `${this.orgUrl}${projectName}/_apis/build/builds?api-version=5.0`;
|
|
1257
1258
|
let res = await TFSServices.postRequest(url, this.token, 'post', data, null);
|
|
1258
1259
|
return res;
|
|
@@ -1331,7 +1332,7 @@ export default class PipelinesDataProvider {
|
|
|
1331
1332
|
}
|
|
1332
1333
|
return res;
|
|
1333
1334
|
} catch (err: any) {
|
|
1334
|
-
logger.error(
|
|
1335
|
+
logger.error('Could not fetch Pipeline Run History:', describeError(err));
|
|
1335
1336
|
}
|
|
1336
1337
|
}
|
|
1337
1338
|
|
|
@@ -1401,7 +1402,7 @@ export default class PipelinesDataProvider {
|
|
|
1401
1402
|
// Azure DevOps returns continuation token header for next page
|
|
1402
1403
|
continuationToken = this.getContinuationToken(headers);
|
|
1403
1404
|
} catch (err: any) {
|
|
1404
|
-
logger.error(
|
|
1405
|
+
logger.error('GetAllReleaseHistory failed:', describeError(err));
|
|
1405
1406
|
throw err;
|
|
1406
1407
|
}
|
|
1407
1408
|
} while (continuationToken);
|
|
@@ -1463,7 +1464,7 @@ export default class PipelinesDataProvider {
|
|
|
1463
1464
|
}
|
|
1464
1465
|
}
|
|
1465
1466
|
} catch (err: any) {
|
|
1466
|
-
logger.error(`Error fetching pipeline ${pipelineRunId} with url ${url}
|
|
1467
|
+
logger.error(`Error fetching pipeline ${pipelineRunId} with url ${url} :`, describeError(err));
|
|
1467
1468
|
return undefined;
|
|
1468
1469
|
}
|
|
1469
1470
|
}
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
MewpRunStatus,
|
|
23
23
|
} from '../models/mewp-reporting';
|
|
24
24
|
import logger from '../utils/logger';
|
|
25
|
+
import { describeError } from '../helpers/requestContext';
|
|
25
26
|
import MewpExternalIngestionUtils from '../utils/mewpExternalIngestionUtils';
|
|
26
27
|
import MewpExternalTableUtils, {
|
|
27
28
|
MewpExternalFileValidationError,
|
|
@@ -139,7 +140,7 @@ export default class ResultDataProvider {
|
|
|
139
140
|
this.fetchTestPoints(projectName, testPlanId, suite.testSuiteId)
|
|
140
141
|
.then((testPointsItems) => ({ ...suite, testPointsItems }))
|
|
141
142
|
.catch((error: any) => {
|
|
142
|
-
logger.error(`Error occurred for suite ${suite.testSuiteId}
|
|
143
|
+
logger.error(`Error occurred for suite ${suite.testSuiteId}:`, describeError(error));
|
|
143
144
|
return { ...suite, testPointsItems: [] };
|
|
144
145
|
})
|
|
145
146
|
)
|
|
@@ -259,10 +260,22 @@ export default class ResultDataProvider {
|
|
|
259
260
|
|
|
260
261
|
return { combinedResults, openPcrToTestCaseTraceMap, testCaseToOpenPcrTraceMap };
|
|
261
262
|
} catch (error: any) {
|
|
262
|
-
|
|
263
|
+
// One event, one record — this used to be two separate calls (message, then
|
|
264
|
+
// conditionally the response body), which interleave with other requests' output
|
|
265
|
+
// under concurrency and become two unrelated entries in the dashboard's error grouping.
|
|
266
|
+
let responseSummary: string | undefined;
|
|
263
267
|
if (error.response) {
|
|
264
|
-
|
|
268
|
+
// Truncated to match the bound already used for this in tfs.ts — an untruncated ADO
|
|
269
|
+
// response body isn't a credential, but it can still be large/sensitive work-item content
|
|
270
|
+
// and doesn't need to go into logs unbounded. JSON.stringify itself (not just the
|
|
271
|
+
// output) can throw on a circular value, so that has to be guarded too, not just capped.
|
|
272
|
+
try {
|
|
273
|
+
responseSummary = JSON.stringify(error.response.data).substring(0, 200);
|
|
274
|
+
} catch {
|
|
275
|
+
responseSummary = '[unserializable response data]';
|
|
276
|
+
}
|
|
265
277
|
}
|
|
278
|
+
logger.error('Error during getCombinedResultsSummary:', { ...describeError(error), responseSummary });
|
|
266
279
|
// Ensure the error is rethrown to propagate it correctly
|
|
267
280
|
throw new Error(error.message || 'Unknown error occurred during getCombinedResultsSummary');
|
|
268
281
|
}
|
|
@@ -390,7 +403,7 @@ export default class ResultDataProvider {
|
|
|
390
403
|
|
|
391
404
|
return fetchedTestResults;
|
|
392
405
|
} catch (error: any) {
|
|
393
|
-
logger.error(
|
|
406
|
+
logger.error('Error during getTestReporterResults:', describeError(error));
|
|
394
407
|
}
|
|
395
408
|
}
|
|
396
409
|
|
|
@@ -431,7 +444,7 @@ export default class ResultDataProvider {
|
|
|
431
444
|
|
|
432
445
|
return { planId: testPlanId, planName, rows: rows || [] };
|
|
433
446
|
} catch (error: any) {
|
|
434
|
-
logger.error(
|
|
447
|
+
logger.error('Error during getTestReporterFlatResults:', describeError(error));
|
|
435
448
|
return { planId: testPlanId, planName: '', rows: [] };
|
|
436
449
|
}
|
|
437
450
|
}
|
|
@@ -733,7 +746,7 @@ export default class ResultDataProvider {
|
|
|
733
746
|
rows,
|
|
734
747
|
};
|
|
735
748
|
} catch (error: any) {
|
|
736
|
-
logger.error(
|
|
749
|
+
logger.error('Error during getMewpL2CoverageFlatResults:', describeError(error));
|
|
737
750
|
if (error instanceof MewpExternalFileValidationError) {
|
|
738
751
|
throw error;
|
|
739
752
|
}
|
|
@@ -1110,7 +1123,7 @@ export default class ResultDataProvider {
|
|
|
1110
1123
|
rows,
|
|
1111
1124
|
};
|
|
1112
1125
|
} catch (error: any) {
|
|
1113
|
-
logger.error(
|
|
1126
|
+
logger.error('Error during getMewpInternalValidationFlatResults:', describeError(error));
|
|
1114
1127
|
return defaultPayload;
|
|
1115
1128
|
}
|
|
1116
1129
|
}
|
|
@@ -3059,7 +3072,7 @@ export default class ResultDataProvider {
|
|
|
3059
3072
|
const testPlan = await TFSServices.getItemContent(url, this.token);
|
|
3060
3073
|
return testPlan.name;
|
|
3061
3074
|
} catch (error: any) {
|
|
3062
|
-
logger.error(
|
|
3075
|
+
logger.error('Error during fetching Test Plan Name:', describeError(error));
|
|
3063
3076
|
return '';
|
|
3064
3077
|
}
|
|
3065
3078
|
}
|
|
@@ -3107,7 +3120,7 @@ export default class ResultDataProvider {
|
|
|
3107
3120
|
};
|
|
3108
3121
|
});
|
|
3109
3122
|
} catch (error: any) {
|
|
3110
|
-
logger.error(
|
|
3123
|
+
logger.error('Error during fetching Test Suites:', describeError(error));
|
|
3111
3124
|
return [];
|
|
3112
3125
|
}
|
|
3113
3126
|
}
|
|
@@ -3261,8 +3274,7 @@ export default class ResultDataProvider {
|
|
|
3261
3274
|
);
|
|
3262
3275
|
return detailedPoints;
|
|
3263
3276
|
} catch (err: any) {
|
|
3264
|
-
logger.error(
|
|
3265
|
-
logger.error(`Error stack: ${err.stack}`);
|
|
3277
|
+
logger.error('Error during fetching Cross Test Points', err);
|
|
3266
3278
|
return [];
|
|
3267
3279
|
}
|
|
3268
3280
|
}
|
|
@@ -3281,7 +3293,7 @@ export default class ResultDataProvider {
|
|
|
3281
3293
|
|
|
3282
3294
|
return count !== 0 ? testPoints.map((testPoint: any) => this.mapTestPoint(testPoint, projectName)) : [];
|
|
3283
3295
|
} catch (error: any) {
|
|
3284
|
-
logger.error(
|
|
3296
|
+
logger.error('Error during fetching Test Points:', describeError(error));
|
|
3285
3297
|
return [];
|
|
3286
3298
|
}
|
|
3287
3299
|
}
|
|
@@ -3903,10 +3915,7 @@ export default class ResultDataProvider {
|
|
|
3903
3915
|
relatedCRs,
|
|
3904
3916
|
};
|
|
3905
3917
|
} catch (error: any) {
|
|
3906
|
-
logger.error(
|
|
3907
|
-
if (isTestReporter) {
|
|
3908
|
-
logger.error(`Error stack: ${error.stack}`);
|
|
3909
|
-
}
|
|
3918
|
+
logger.error('Error while fetching run result', error);
|
|
3910
3919
|
return null;
|
|
3911
3920
|
}
|
|
3912
3921
|
}
|
|
@@ -4465,7 +4474,7 @@ export default class ResultDataProvider {
|
|
|
4465
4474
|
relatedCRs.push({ id, title, workItemType: 'Change Request', url });
|
|
4466
4475
|
}
|
|
4467
4476
|
} catch (err: any) {
|
|
4468
|
-
logger.error(`Could not append related work item to test case ${wiByRevision.id}
|
|
4477
|
+
logger.error(`Could not append related work item to test case ${wiByRevision.id}:`, describeError(err));
|
|
4469
4478
|
}
|
|
4470
4479
|
}
|
|
4471
4480
|
}
|
|
@@ -4595,7 +4604,7 @@ export default class ResultDataProvider {
|
|
|
4595
4604
|
? testCase.workItem.workItemFields
|
|
4596
4605
|
: [];
|
|
4597
4606
|
if (testCaseWorkItemFields.length === 0) {
|
|
4598
|
-
logger.warn(`Could not fetch the steps from WI ${
|
|
4607
|
+
logger.warn(`Could not fetch the steps from WI ${testCase.workItem.id}`);
|
|
4599
4608
|
if (!isTestReporter) {
|
|
4600
4609
|
continue;
|
|
4601
4610
|
}
|
|
@@ -4848,7 +4857,7 @@ export default class ResultDataProvider {
|
|
|
4848
4857
|
|
|
4849
4858
|
return { ...suite, testPointsItems, testCasesItems };
|
|
4850
4859
|
} catch (error: any) {
|
|
4851
|
-
logger.error(`Error occurred for suite ${suite.testSuiteId}
|
|
4860
|
+
logger.error(`Error occurred for suite ${suite.testSuiteId}:`, describeError(error));
|
|
4852
4861
|
return suite;
|
|
4853
4862
|
}
|
|
4854
4863
|
})
|
|
@@ -5058,8 +5067,7 @@ export default class ResultDataProvider {
|
|
|
5058
5067
|
? createResponseObject(resultData, testSuiteId, point, ...additionalArgs)
|
|
5059
5068
|
: null;
|
|
5060
5069
|
} catch (error: any) {
|
|
5061
|
-
logger.error(`Error occurred for point ${point.testCaseId}
|
|
5062
|
-
logger.error(`Stack trace: ${error.stack}`);
|
|
5070
|
+
logger.error(`Error occurred for point ${point.testCaseId}`, error);
|
|
5063
5071
|
return null;
|
|
5064
5072
|
}
|
|
5065
5073
|
}
|
|
@@ -5153,8 +5161,7 @@ export default class ResultDataProvider {
|
|
|
5153
5161
|
summarizedItemMap.set(wi.id, mappedItem);
|
|
5154
5162
|
}
|
|
5155
5163
|
} catch (error: any) {
|
|
5156
|
-
logger.error(
|
|
5157
|
-
logger.error(`Error Stack: ${error.stack}`);
|
|
5164
|
+
logger.error('Error occurred while fetching linked work items', error);
|
|
5158
5165
|
}
|
|
5159
5166
|
}
|
|
5160
5167
|
|
|
@@ -5826,8 +5833,7 @@ export default class ResultDataProvider {
|
|
|
5826
5833
|
}
|
|
5827
5834
|
return resultDataResponse;
|
|
5828
5835
|
} catch (err: any) {
|
|
5829
|
-
logger.error(
|
|
5830
|
-
logger.error(`Error stack: ${err.stack}`);
|
|
5836
|
+
logger.error('Error occurred while fetching result data', err);
|
|
5831
5837
|
return null;
|
|
5832
5838
|
}
|
|
5833
5839
|
},
|
|
@@ -4,6 +4,7 @@ import { TestSteps, createMomRelation, createRequirementRelation } from '../mode
|
|
|
4
4
|
import { TestCase } from '../models/tfs-data';
|
|
5
5
|
import * as xml2js from 'xml2js';
|
|
6
6
|
import logger from '../utils/logger';
|
|
7
|
+
import { describeError } from '../helpers/requestContext';
|
|
7
8
|
import Utils from '../utils/testStepParserHelper';
|
|
8
9
|
import DataProviderUtils from '../utils/DataProviderUtils';
|
|
9
10
|
const pLimit = require('p-limit');
|
|
@@ -56,7 +57,7 @@ export default class TestDataProvider {
|
|
|
56
57
|
|
|
57
58
|
return result;
|
|
58
59
|
} catch (error: any) {
|
|
59
|
-
logger.error(`Error fetching ${url}
|
|
60
|
+
logger.error(`Error fetching ${url}:`, describeError(error));
|
|
60
61
|
throw error;
|
|
61
62
|
}
|
|
62
63
|
}
|
|
@@ -187,7 +188,7 @@ export default class TestDataProvider {
|
|
|
187
188
|
try {
|
|
188
189
|
return await this.fetchWithCache(testsuitesUrl);
|
|
189
190
|
} catch (e: any) {
|
|
190
|
-
logger.error(
|
|
191
|
+
logger.error('Failed to get test suites:', describeError(e));
|
|
191
192
|
return null;
|
|
192
193
|
}
|
|
193
194
|
}
|
|
@@ -599,7 +600,7 @@ export default class TestDataProvider {
|
|
|
599
600
|
}
|
|
600
601
|
}
|
|
601
602
|
} catch (err: any) {
|
|
602
|
-
logger.error(`Error
|
|
603
|
+
logger.error(`Error while trying to structure testCases for test suite ${suite.id}:`, describeError(err));
|
|
603
604
|
}
|
|
604
605
|
|
|
605
606
|
return testCasesUrlList;
|