@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
|
@@ -169,6 +169,117 @@ describe('DiagnosticsTransport (Phase 6a)', () => {
|
|
|
169
169
|
});
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
+
describe('DiagnosticsTransport request context', () => {
|
|
173
|
+
function makeLogger() {
|
|
174
|
+
return winston.createLogger({
|
|
175
|
+
level: 'silly',
|
|
176
|
+
format: winston.format.combine(
|
|
177
|
+
winston.format.errors({ stack: true }),
|
|
178
|
+
winston.format.timestamp(),
|
|
179
|
+
redact(),
|
|
180
|
+
winston.format.splat(),
|
|
181
|
+
winston.format.json()
|
|
182
|
+
),
|
|
183
|
+
transports: [new DiagnosticsTransport()],
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
test('persists an annotated error\'s request as event.context, however the call site words its message', () => {
|
|
188
|
+
const events: DiagnosticEvent[] = [];
|
|
189
|
+
installLogSink({ push: (e) => events.push(e) });
|
|
190
|
+
const err = Object.assign(new Error('Request failed with status code 404'), {
|
|
191
|
+
adoRequest: { method: 'GET', url: 'https://dev.azure.com/org/_apis/x', status: 404, attempt: 1, responseExcerpt: 'nope' },
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
makeLogger().error(err); // the bare-error shape (3 call sites) — no stable message at all
|
|
195
|
+
|
|
196
|
+
expect(events[0].context).toEqual({
|
|
197
|
+
method: 'GET',
|
|
198
|
+
url: 'https://dev.azure.com/org/_apis/x',
|
|
199
|
+
status: 404,
|
|
200
|
+
attempt: 1,
|
|
201
|
+
responseExcerpt: 'nope',
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('also works for the stable-message + error shape and for the explicit meta shape', () => {
|
|
206
|
+
const events: DiagnosticEvent[] = [];
|
|
207
|
+
installLogSink({ push: (e) => events.push(e) });
|
|
208
|
+
const logger = makeLogger();
|
|
209
|
+
const ctx = { method: 'POST', url: 'https://h/wiql', requestBody: '{"query":"q"}' };
|
|
210
|
+
|
|
211
|
+
logger.error('Failed querying work items', Object.assign(new Error('boom'), { adoRequest: ctx }));
|
|
212
|
+
logger.error('ADO request failed', { message: 'boom', stack: 'at x', adoRequest: ctx });
|
|
213
|
+
|
|
214
|
+
expect(events[0].context).toEqual(ctx);
|
|
215
|
+
expect(events[1].context).toEqual(ctx);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('an event with no request context has no context field', () => {
|
|
219
|
+
const events: DiagnosticEvent[] = [];
|
|
220
|
+
installLogSink({ push: (e) => events.push(e) });
|
|
221
|
+
makeLogger().error('plain failure', new Error('boom'));
|
|
222
|
+
expect(events[0].context).toBeUndefined();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('re-validates the shape at the transport: unknown keys, wrong types and oversize values are dropped or bounded', () => {
|
|
226
|
+
const events: DiagnosticEvent[] = [];
|
|
227
|
+
installLogSink({ push: (e) => events.push(e) });
|
|
228
|
+
const err = Object.assign(new Error('x'), {
|
|
229
|
+
adoRequest: {
|
|
230
|
+
method: 'GET',
|
|
231
|
+
url: 'u'.repeat(5000),
|
|
232
|
+
status: 'not-a-number',
|
|
233
|
+
attempt: 2,
|
|
234
|
+
requestBody: 'b'.repeat(5000),
|
|
235
|
+
secretHeader: 'Bearer abc',
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
makeLogger().error('m', err);
|
|
240
|
+
|
|
241
|
+
const ctx = events[0].context!;
|
|
242
|
+
expect(ctx.url!.length).toBe(1000);
|
|
243
|
+
expect(ctx.requestBody!.length).toBe(2000);
|
|
244
|
+
expect(ctx.status).toBeUndefined();
|
|
245
|
+
expect(ctx.attempt).toBe(2);
|
|
246
|
+
expect(ctx).not.toHaveProperty('secretHeader');
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('text format request suffix', () => {
|
|
251
|
+
// The default (LOG_FORMAT unset) prints only the message — without this the URL would never
|
|
252
|
+
// reach stdout for a record whose own message doesn't carry it. A stream transport on the
|
|
253
|
+
// real logger receives the already-formatted line, independent of how jest wires console.
|
|
254
|
+
test('appends the request to the printed line when the error carries one, and never throws', () => {
|
|
255
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
256
|
+
const logger = require('../../utils/logger').default;
|
|
257
|
+
const { Writable } = require('stream');
|
|
258
|
+
const lines: string[] = [];
|
|
259
|
+
const sink = new winston.transports.Stream({
|
|
260
|
+
stream: new Writable({
|
|
261
|
+
write(chunk: Buffer, _enc: string, cb: () => void) {
|
|
262
|
+
lines.push(chunk.toString());
|
|
263
|
+
cb();
|
|
264
|
+
},
|
|
265
|
+
}),
|
|
266
|
+
});
|
|
267
|
+
logger.add(sink);
|
|
268
|
+
try {
|
|
269
|
+
const err = Object.assign(new Error('Request failed with status code 404'), {
|
|
270
|
+
adoRequest: { method: 'GET', url: 'https://dev.azure.com/org/_apis/x', status: 404 },
|
|
271
|
+
});
|
|
272
|
+
expect(() => logger.error('Failed fetching', err)).not.toThrow();
|
|
273
|
+
expect(() => logger.error('odd', Object.assign(new Error('e'), { adoRequest: { url: 123 } }))).not.toThrow();
|
|
274
|
+
} finally {
|
|
275
|
+
logger.remove(sink);
|
|
276
|
+
}
|
|
277
|
+
const joined = lines.join('');
|
|
278
|
+
expect(joined).toContain('[GET https://dev.azure.com/org/_apis/x -> 404]');
|
|
279
|
+
expect(joined).toContain('Failed fetching');
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
172
283
|
describe('textFormat safety (regression)', () => {
|
|
173
284
|
// The hostile-input matrix above exercises makeTestLogger()'s json() pipeline — it never
|
|
174
285
|
// touched the real singleton logger's text-format branch (LOG_FORMAT's default), which is
|
package/src/utils/logSink.ts
CHANGED
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
// The one queryable-error-store event shape, emitted by DiagnosticsTransport (logger.ts).
|
|
4
4
|
// This package never installs a sink itself — it only reads whatever the host process
|
|
5
|
-
// (docgen-content-control's index.ts) installed. err
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// (docgen-content-control's index.ts) installed. err stays narrow — {message, code, stack}.
|
|
6
|
+
// `context` is the one deliberate exception to "no extra-fields bucket": a sanitized
|
|
7
|
+
// description of the failed outbound request (method, url, status, attempt, request-body
|
|
8
|
+
// summary, response excerpt) — without it a 404 in the dashboard can't be traced to the call
|
|
9
|
+
// that produced it. A fixed, allowlisted shape, not a free-form bag.
|
|
10
|
+
// Bounds for DiagnosticEvent.context. Single source for this package (requestContext.ts and the
|
|
11
|
+
// logger transport); docgen-api-gate's DiagnosticsController.sanitizeContext re-validates at
|
|
12
|
+
// ingest with its own copy — keep the two in step.
|
|
13
|
+
export const CONTEXT_LIMITS = { method: 10, url: 1000, requestBody: 2000, responseExcerpt: 300 } as const;
|
|
14
|
+
|
|
8
15
|
export interface DiagnosticEvent {
|
|
9
16
|
ts: string;
|
|
10
17
|
level: string;
|
|
@@ -19,6 +26,14 @@ export interface DiagnosticEvent {
|
|
|
19
26
|
userId?: string;
|
|
20
27
|
message: string;
|
|
21
28
|
err?: { message: string; code?: string; stack?: string };
|
|
29
|
+
context?: {
|
|
30
|
+
method?: string;
|
|
31
|
+
url?: string;
|
|
32
|
+
status?: number;
|
|
33
|
+
attempt?: number;
|
|
34
|
+
requestBody?: string;
|
|
35
|
+
responseExcerpt?: string;
|
|
36
|
+
};
|
|
22
37
|
// Phase 6b — set on a debug/info event captured under retain-on-failure. Deleted by
|
|
23
38
|
// api-gate at the run's one success point; left alone (and thus permanent, subject to the
|
|
24
39
|
// normal TTL) if the run fails.
|
package/src/utils/logger.ts
CHANGED
|
@@ -4,7 +4,7 @@ import * as fs from 'fs';
|
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import * as Transport from 'winston-transport';
|
|
6
6
|
import { runContextStore } from './runContext';
|
|
7
|
-
import { getLogSink, DiagnosticEvent } from './logSink';
|
|
7
|
+
import { getLogSink, DiagnosticEvent, CONTEXT_LIMITS } from './logSink';
|
|
8
8
|
let logger: winston.Logger;
|
|
9
9
|
|
|
10
10
|
// Merges the ambient runId (set by content-control's request middleware, once Phase 3 lands
|
|
@@ -44,7 +44,7 @@ function safeRead(obj: Record<string, unknown>, key: string): { ok: true; value:
|
|
|
44
44
|
return { ok: false };
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
function redactValue(value: unknown, depth = 0, seen = new WeakSet<object>()): unknown {
|
|
47
|
+
export function redactValue(value: unknown, depth = 0, seen = new WeakSet<object>()): unknown {
|
|
48
48
|
if (depth > 6 || value === null || typeof value !== 'object') return value;
|
|
49
49
|
if (seen.has(value as object)) return '[Circular]';
|
|
50
50
|
seen.add(value as object);
|
|
@@ -116,7 +116,26 @@ function readOwnVersion(): string {
|
|
|
116
116
|
const safeMessageString = (value: unknown): string =>
|
|
117
117
|
typeof value === 'symbol' ? value.toString() : String(value);
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
// A failed ADO request's description (helpers/requestContext.ts), attached to the thrown error
|
|
120
|
+
// and merged onto the record by winston. In text mode — the default, which prints only the
|
|
121
|
+
// message — this is the only way the URL reaches stdout for a record whose own message doesn't
|
|
122
|
+
// carry it. Never throws: this runs inside the formatter, which has no try/catch around it.
|
|
123
|
+
const requestSuffix = (ctx: unknown): string => {
|
|
124
|
+
try {
|
|
125
|
+
if (!ctx || typeof ctx !== 'object') return '';
|
|
126
|
+
const c = ctx as Record<string, unknown>;
|
|
127
|
+
if (typeof c.url !== 'string') return '';
|
|
128
|
+
const method = typeof c.method === 'string' ? `${c.method} ` : '';
|
|
129
|
+
const status = typeof c.status === 'number' ? ` -> ${c.status}` : '';
|
|
130
|
+
return ` [${method}${c.url}${status}]`;
|
|
131
|
+
} catch {
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const textFormat = winston.format.printf(
|
|
137
|
+
(info) => `${info.timestamp} - ${info.level}: ${safeMessageString(info.message)}${requestSuffix(info.adoRequest)}`
|
|
138
|
+
);
|
|
120
139
|
|
|
121
140
|
// Bounded so one oversized message/stack can't produce an unbounded LogEvent document.
|
|
122
141
|
const MAX_MESSAGE_LEN = 2000;
|
|
@@ -126,6 +145,26 @@ function clamp(value: unknown, max: number): string | undefined {
|
|
|
126
145
|
return value.length > max ? value.slice(0, max) : value;
|
|
127
146
|
}
|
|
128
147
|
|
|
148
|
+
// Re-validates the request description at the transport instead of trusting its shape — it
|
|
149
|
+
// arrives via an Error's own properties, which anything upstream could have set. Allowlisted
|
|
150
|
+
// keys only, each type-checked and bounded.
|
|
151
|
+
function pickContext(raw: unknown): DiagnosticEvent['context'] {
|
|
152
|
+
if (!raw || typeof raw !== 'object') return undefined;
|
|
153
|
+
const c = raw as Record<string, unknown>;
|
|
154
|
+
const context: NonNullable<DiagnosticEvent['context']> = {};
|
|
155
|
+
const method = clamp(c.method, CONTEXT_LIMITS.method);
|
|
156
|
+
const url = clamp(c.url, CONTEXT_LIMITS.url);
|
|
157
|
+
const requestBody = clamp(c.requestBody, CONTEXT_LIMITS.requestBody);
|
|
158
|
+
const responseExcerpt = clamp(c.responseExcerpt, CONTEXT_LIMITS.responseExcerpt);
|
|
159
|
+
if (method) context.method = method;
|
|
160
|
+
if (url) context.url = url;
|
|
161
|
+
if (requestBody) context.requestBody = requestBody;
|
|
162
|
+
if (responseExcerpt) context.responseExcerpt = responseExcerpt;
|
|
163
|
+
if (typeof c.status === 'number' && Number.isFinite(c.status)) context.status = c.status;
|
|
164
|
+
if (typeof c.attempt === 'number' && Number.isFinite(c.attempt)) context.attempt = c.attempt;
|
|
165
|
+
return Object.keys(context).length ? context : undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
129
168
|
// Ships warn/error records to whatever LogSink the host process (docgen-content-control's
|
|
130
169
|
// index.ts) has installed — this package never installs one itself, only reads it. This is
|
|
131
170
|
// the dominant source of real ADO error records (tfs.ts's executeWithRetry is the single
|
|
@@ -182,6 +221,7 @@ export class DiagnosticsTransport extends Transport {
|
|
|
182
221
|
stack: clamp(info.stack, MAX_STACK_LEN),
|
|
183
222
|
}
|
|
184
223
|
: undefined,
|
|
224
|
+
context: pickContext(info.adoRequest),
|
|
185
225
|
retainPending: !isWarnOrError && captureMode === 'retain-on-failure' ? true : undefined,
|
|
186
226
|
};
|
|
187
227
|
getLogSink()?.push(event);
|