@elisra-devops/docgen-data-provider 1.141.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/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 +22 -29
- 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 +2 -1
- package/bin/modules/JfrogDataProvider.js.map +1 -1
- package/bin/modules/PipelinesDataProvider.js +4 -3
- package/bin/modules/PipelinesDataProvider.js.map +1 -1
- package/bin/modules/ResultDataProvider.js +12 -11
- 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 +13 -12
- 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 +1 -1
- package/bin/tests/modules/JfrogDataProvider.test.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +4 -2
- 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.js +94 -0
- package/bin/tests/utils/logger.test.js.map +1 -1
- package/bin/utils/logSink.d.ts +14 -0
- package/bin/utils/logSink.js +12 -0
- package/bin/utils/logSink.js.map +1 -1
- package/bin/utils/logger.d.ts +1 -0
- package/bin/utils/logger.js +48 -1
- package/bin/utils/logger.js.map +1 -1
- package/package.json +1 -1
- package/src/helpers/requestContext.ts +222 -0
- package/src/helpers/tfs.ts +28 -26
- package/src/modules/GitDataProvider.ts +3 -2
- package/src/modules/JfrogDataProvider.ts +2 -1
- package/src/modules/PipelinesDataProvider.ts +4 -3
- package/src/modules/ResultDataProvider.ts +12 -11
- package/src/modules/TestDataProvider.ts +4 -3
- package/src/modules/TicketsDataProvider.ts +13 -12
- 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 +2 -1
- package/src/tests/modules/ResultDataProvider.test.ts +5 -2
- 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 +111 -0
- package/src/utils/logSink.ts +18 -3
- package/src/utils/logger.ts +43 -3
package/bin/utils/logSink.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export declare const CONTEXT_LIMITS: {
|
|
2
|
+
readonly method: 10;
|
|
3
|
+
readonly url: 1000;
|
|
4
|
+
readonly requestBody: 2000;
|
|
5
|
+
readonly responseExcerpt: 300;
|
|
6
|
+
};
|
|
1
7
|
export interface DiagnosticEvent {
|
|
2
8
|
ts: string;
|
|
3
9
|
level: string;
|
|
@@ -16,6 +22,14 @@ export interface DiagnosticEvent {
|
|
|
16
22
|
code?: string;
|
|
17
23
|
stack?: string;
|
|
18
24
|
};
|
|
25
|
+
context?: {
|
|
26
|
+
method?: string;
|
|
27
|
+
url?: string;
|
|
28
|
+
status?: number;
|
|
29
|
+
attempt?: number;
|
|
30
|
+
requestBody?: string;
|
|
31
|
+
responseExcerpt?: string;
|
|
32
|
+
};
|
|
19
33
|
retainPending?: boolean;
|
|
20
34
|
}
|
|
21
35
|
export interface LogSink {
|
package/bin/utils/logSink.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CONTEXT_LIMITS = void 0;
|
|
3
4
|
exports.getLogSink = getLogSink;
|
|
4
5
|
exports.installLogSink = installLogSink;
|
|
6
|
+
// The one queryable-error-store event shape, emitted by DiagnosticsTransport (logger.ts).
|
|
7
|
+
// This package never installs a sink itself — it only reads whatever the host process
|
|
8
|
+
// (docgen-content-control's index.ts) installed. err stays narrow — {message, code, stack}.
|
|
9
|
+
// `context` is the one deliberate exception to "no extra-fields bucket": a sanitized
|
|
10
|
+
// description of the failed outbound request (method, url, status, attempt, request-body
|
|
11
|
+
// summary, response excerpt) — without it a 404 in the dashboard can't be traced to the call
|
|
12
|
+
// that produced it. A fixed, allowlisted shape, not a free-form bag.
|
|
13
|
+
// Bounds for DiagnosticEvent.context. Single source for this package (requestContext.ts and the
|
|
14
|
+
// logger transport); docgen-api-gate's DiagnosticsController.sanitizeContext re-validates at
|
|
15
|
+
// ingest with its own copy — keep the two in step.
|
|
16
|
+
exports.CONTEXT_LIMITS = { method: 10, url: 1000, requestBody: 2000, responseExcerpt: 300 };
|
|
5
17
|
// Symbol.for so this package's copy converges on the same installed sink as
|
|
6
18
|
// docgen-content-control's (the host process this package runs inside) and
|
|
7
19
|
// docgen-dg-skins-package's, the same reasoning as runContext.ts's AsyncLocalStorage. A
|
package/bin/utils/logSink.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logSink.js","sourceRoot":"","sources":["../../src/utils/logSink.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC
|
|
1
|
+
{"version":3,"file":"logSink.js","sourceRoot":"","sources":["../../src/utils/logSink.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;;;AAoDb,gCAEC;AAED,wCAEC;AAxDD,0FAA0F;AAC1F,sFAAsF;AACtF,4FAA4F;AAC5F,qFAAqF;AACrF,yFAAyF;AACzF,6FAA6F;AAC7F,qEAAqE;AACrE,gGAAgG;AAChG,6FAA6F;AAC7F,mDAAmD;AACtC,QAAA,cAAc,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe,EAAE,GAAG,EAAW,CAAC;AAkC1G,4EAA4E;AAC5E,2EAA2E;AAC3E,wFAAwF;AACxF,6FAA6F;AAC7F,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC;AAEtD,SAAgB,UAAU;IACxB,OAAQ,UAAsC,CAAC,GAAG,CAAwB,CAAC;AAC7E,CAAC;AAED,SAAgB,cAAc,CAAC,IAAa;IACzC,UAAsC,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;AACtD,CAAC"}
|
package/bin/utils/logger.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as winston from 'winston';
|
|
|
2
2
|
import * as Transport from 'winston-transport';
|
|
3
3
|
declare let logger: winston.Logger;
|
|
4
4
|
export declare const withRunContext: winston.Logform.FormatWrap;
|
|
5
|
+
export declare function redactValue(value: unknown, depth?: number, seen?: WeakSet<object>): unknown;
|
|
5
6
|
export declare const redact: winston.Logform.FormatWrap;
|
|
6
7
|
export declare class DiagnosticsTransport extends Transport {
|
|
7
8
|
log(info: Record<string, unknown>, callback: () => void): void;
|
package/bin/utils/logger.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DiagnosticsTransport = exports.redact = exports.withRunContext = void 0;
|
|
4
|
+
exports.redactValue = redactValue;
|
|
4
5
|
const winston = require("winston");
|
|
5
6
|
const fs = require("fs");
|
|
6
7
|
const path = require("path");
|
|
@@ -122,7 +123,26 @@ function readOwnVersion() {
|
|
|
122
123
|
// crash inside this formatter itself, the one place in the pipeline with no try/catch around
|
|
123
124
|
// it. Symbol.toString() itself never throws, so this needs no further guarding.
|
|
124
125
|
const safeMessageString = (value) => typeof value === 'symbol' ? value.toString() : String(value);
|
|
125
|
-
|
|
126
|
+
// A failed ADO request's description (helpers/requestContext.ts), attached to the thrown error
|
|
127
|
+
// and merged onto the record by winston. In text mode — the default, which prints only the
|
|
128
|
+
// message — this is the only way the URL reaches stdout for a record whose own message doesn't
|
|
129
|
+
// carry it. Never throws: this runs inside the formatter, which has no try/catch around it.
|
|
130
|
+
const requestSuffix = (ctx) => {
|
|
131
|
+
try {
|
|
132
|
+
if (!ctx || typeof ctx !== 'object')
|
|
133
|
+
return '';
|
|
134
|
+
const c = ctx;
|
|
135
|
+
if (typeof c.url !== 'string')
|
|
136
|
+
return '';
|
|
137
|
+
const method = typeof c.method === 'string' ? `${c.method} ` : '';
|
|
138
|
+
const status = typeof c.status === 'number' ? ` -> ${c.status}` : '';
|
|
139
|
+
return ` [${method}${c.url}${status}]`;
|
|
140
|
+
}
|
|
141
|
+
catch (_a) {
|
|
142
|
+
return '';
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const textFormat = winston.format.printf((info) => `${info.timestamp} - ${info.level}: ${safeMessageString(info.message)}${requestSuffix(info.adoRequest)}`);
|
|
126
146
|
// Bounded so one oversized message/stack can't produce an unbounded LogEvent document.
|
|
127
147
|
const MAX_MESSAGE_LEN = 2000;
|
|
128
148
|
const MAX_STACK_LEN = 4000;
|
|
@@ -131,6 +151,32 @@ function clamp(value, max) {
|
|
|
131
151
|
return undefined;
|
|
132
152
|
return value.length > max ? value.slice(0, max) : value;
|
|
133
153
|
}
|
|
154
|
+
// Re-validates the request description at the transport instead of trusting its shape — it
|
|
155
|
+
// arrives via an Error's own properties, which anything upstream could have set. Allowlisted
|
|
156
|
+
// keys only, each type-checked and bounded.
|
|
157
|
+
function pickContext(raw) {
|
|
158
|
+
if (!raw || typeof raw !== 'object')
|
|
159
|
+
return undefined;
|
|
160
|
+
const c = raw;
|
|
161
|
+
const context = {};
|
|
162
|
+
const method = clamp(c.method, logSink_1.CONTEXT_LIMITS.method);
|
|
163
|
+
const url = clamp(c.url, logSink_1.CONTEXT_LIMITS.url);
|
|
164
|
+
const requestBody = clamp(c.requestBody, logSink_1.CONTEXT_LIMITS.requestBody);
|
|
165
|
+
const responseExcerpt = clamp(c.responseExcerpt, logSink_1.CONTEXT_LIMITS.responseExcerpt);
|
|
166
|
+
if (method)
|
|
167
|
+
context.method = method;
|
|
168
|
+
if (url)
|
|
169
|
+
context.url = url;
|
|
170
|
+
if (requestBody)
|
|
171
|
+
context.requestBody = requestBody;
|
|
172
|
+
if (responseExcerpt)
|
|
173
|
+
context.responseExcerpt = responseExcerpt;
|
|
174
|
+
if (typeof c.status === 'number' && Number.isFinite(c.status))
|
|
175
|
+
context.status = c.status;
|
|
176
|
+
if (typeof c.attempt === 'number' && Number.isFinite(c.attempt))
|
|
177
|
+
context.attempt = c.attempt;
|
|
178
|
+
return Object.keys(context).length ? context : undefined;
|
|
179
|
+
}
|
|
134
180
|
// Ships warn/error records to whatever LogSink the host process (docgen-content-control's
|
|
135
181
|
// index.ts) has installed — this package never installs one itself, only reads it. This is
|
|
136
182
|
// the dominant source of real ADO error records (tfs.ts's executeWithRetry is the single
|
|
@@ -187,6 +233,7 @@ class DiagnosticsTransport extends Transport {
|
|
|
187
233
|
stack: clamp(info.stack, MAX_STACK_LEN),
|
|
188
234
|
}
|
|
189
235
|
: undefined,
|
|
236
|
+
context: pickContext(info.adoRequest),
|
|
190
237
|
retainPending: !isWarnOrError && captureMode === 'retain-on-failure' ? true : undefined,
|
|
191
238
|
};
|
|
192
239
|
(_e = (0, logSink_1.getLogSink)()) === null || _e === void 0 ? void 0 : _e.push(event);
|
package/bin/utils/logger.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;;;
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -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,28 +284,30 @@ 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
|
-
}
|
|
300
|
-
}
|
|
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);
|
|
301
300
|
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Log detailed error information
|
|
305
|
+
*/
|
|
306
|
+
private static logDetailedError(error: any, context: AdoRequestContext): void {
|
|
302
307
|
logger.error('ADO request failed', {
|
|
303
|
-
message: error
|
|
304
|
-
stack: error
|
|
305
|
-
|
|
306
|
-
status: error.response?.status,
|
|
307
|
-
responseExcerpt,
|
|
308
|
-
attempt,
|
|
308
|
+
message: error?.message,
|
|
309
|
+
stack: error?.stack,
|
|
310
|
+
adoRequest: context,
|
|
309
311
|
});
|
|
310
312
|
}
|
|
311
313
|
|
|
@@ -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 = '';
|
|
@@ -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;
|
|
@@ -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
|
}
|