@elisra-devops/docgen-data-provider 1.141.0 → 1.143.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.
Files changed (63) hide show
  1. package/bin/helpers/requestContext.d.ts +66 -0
  2. package/bin/helpers/requestContext.js +255 -0
  3. package/bin/helpers/requestContext.js.map +1 -0
  4. package/bin/helpers/tfs.d.ts +5 -0
  5. package/bin/helpers/tfs.js +31 -31
  6. package/bin/helpers/tfs.js.map +1 -1
  7. package/bin/modules/GitDataProvider.js +3 -2
  8. package/bin/modules/GitDataProvider.js.map +1 -1
  9. package/bin/modules/JfrogDataProvider.js +2 -1
  10. package/bin/modules/JfrogDataProvider.js.map +1 -1
  11. package/bin/modules/PipelinesDataProvider.js +4 -3
  12. package/bin/modules/PipelinesDataProvider.js.map +1 -1
  13. package/bin/modules/ResultDataProvider.js +12 -11
  14. package/bin/modules/ResultDataProvider.js.map +1 -1
  15. package/bin/modules/TestDataProvider.js +4 -3
  16. package/bin/modules/TestDataProvider.js.map +1 -1
  17. package/bin/modules/TicketsDataProvider.js +13 -12
  18. package/bin/modules/TicketsDataProvider.js.map +1 -1
  19. package/bin/tests/helpers/requestContext.test.d.ts +1 -0
  20. package/bin/tests/helpers/requestContext.test.js +204 -0
  21. package/bin/tests/helpers/requestContext.test.js.map +1 -0
  22. package/bin/tests/helpers/tfs.test.js +73 -0
  23. package/bin/tests/helpers/tfs.test.js.map +1 -1
  24. package/bin/tests/modules/JfrogDataProvider.test.js +1 -1
  25. package/bin/tests/modules/JfrogDataProvider.test.js.map +1 -1
  26. package/bin/tests/modules/ResultDataProvider.test.js +5 -2
  27. package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
  28. package/bin/tests/modules/gitDataProvider.test.js +1 -1
  29. package/bin/tests/modules/gitDataProvider.test.js.map +1 -1
  30. package/bin/tests/modules/pipelineDataProvider.test.js +1 -1
  31. package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -1
  32. package/bin/tests/modules/testDataProvider.test.js +1 -1
  33. package/bin/tests/modules/testDataProvider.test.js.map +1 -1
  34. package/bin/tests/modules/ticketsDataProvider.test.js +2 -2
  35. package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -1
  36. package/bin/tests/utils/logger.test.js +120 -0
  37. package/bin/tests/utils/logger.test.js.map +1 -1
  38. package/bin/utils/logSink.d.ts +14 -0
  39. package/bin/utils/logSink.js +12 -0
  40. package/bin/utils/logSink.js.map +1 -1
  41. package/bin/utils/logger.d.ts +3 -0
  42. package/bin/utils/logger.js +96 -10
  43. package/bin/utils/logger.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/helpers/requestContext.ts +258 -0
  46. package/src/helpers/tfs.ts +38 -28
  47. package/src/modules/GitDataProvider.ts +3 -2
  48. package/src/modules/JfrogDataProvider.ts +2 -1
  49. package/src/modules/PipelinesDataProvider.ts +4 -3
  50. package/src/modules/ResultDataProvider.ts +12 -11
  51. package/src/modules/TestDataProvider.ts +4 -3
  52. package/src/modules/TicketsDataProvider.ts +13 -12
  53. package/src/tests/helpers/requestContext.test.ts +252 -0
  54. package/src/tests/helpers/tfs.test.ts +107 -0
  55. package/src/tests/modules/JfrogDataProvider.test.ts +2 -1
  56. package/src/tests/modules/ResultDataProvider.test.ts +6 -2
  57. package/src/tests/modules/gitDataProvider.test.ts +2 -1
  58. package/src/tests/modules/pipelineDataProvider.test.ts +2 -1
  59. package/src/tests/modules/testDataProvider.test.ts +4 -1
  60. package/src/tests/modules/ticketsDataProvider.test.ts +2 -2
  61. package/src/tests/utils/logger.test.ts +146 -1
  62. package/src/utils/logSink.ts +18 -3
  63. package/src/utils/logger.ts +85 -7
@@ -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
@@ -29,7 +29,24 @@ export const withRunContext = winston.format((info) => {
29
29
  // "accesskey" (not just "minioaccesskey") so this also catches an *AccessKeyId-style field —
30
30
  // a real gap found in api-gate's copy during the Phase 5 manifest work: only the *SecretKey
31
31
  // sibling was covered before, via "secret". Applied here too to keep the four copies in sync.
32
- const SENSITIVE_KEY = /token|pat|password|secret|authorization|accesskey|minioaccesskey|miniosecretkey/i;
32
+ // Word-based, not substring: the key is split into lower-case words (camelCase, kebab-case and
33
+ // snake_case alike) and judged by how it *ends*. A substring test matched "pat" inside
34
+ // "areaPath" / "path" / "patch" and blanked ordinary ADO fields; this keeps `accessToken`,
35
+ // `x-docgen-ingest-token`, `minioSecretKey`, `PAT`, `password` redacted while `areaPath` stays.
36
+ const SENSITIVE_TAILS = ['token', 'password', 'secret', 'authorization', 'accesskey', 'secretkey', 'apikey'];
37
+ const SENSITIVE_LAST_WORDS = new Set(['pat', 'pwd', 'cookie', 'tokens', 'passwords', 'secrets']);
38
+ export function isSensitiveKey(key: string): boolean {
39
+ const words = key
40
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
41
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
42
+ .toLowerCase()
43
+ .split(/[^a-z0-9]+/)
44
+ .filter(Boolean);
45
+ if (words.length === 0) return false;
46
+ if (SENSITIVE_LAST_WORDS.has(words[words.length - 1])) return true;
47
+ const tail = words.slice(-2).join('');
48
+ return SENSITIVE_TAILS.some((t) => tail.endsWith(t));
49
+ }
33
50
 
34
51
  // A per-key try/catch on the *read*, not just around the whole loop: a getter that throws
35
52
  // (e.g. `{ get boom() { throw ... } }`) would otherwise abort redaction for every remaining
@@ -44,7 +61,7 @@ function safeRead(obj: Record<string, unknown>, key: string): { ok: true; value:
44
61
  return { ok: false };
45
62
  }
46
63
  }
47
- function redactValue(value: unknown, depth = 0, seen = new WeakSet<object>()): unknown {
64
+ export function redactValue(value: unknown, depth = 0, seen = new WeakSet<object>()): unknown {
48
65
  if (depth > 6 || value === null || typeof value !== 'object') return value;
49
66
  if (seen.has(value as object)) return '[Circular]';
50
67
  seen.add(value as object);
@@ -56,7 +73,7 @@ function redactValue(value: unknown, depth = 0, seen = new WeakSet<object>()): u
56
73
  out[key] = '[Unreadable property]';
57
74
  continue;
58
75
  }
59
- out[key] = SENSITIVE_KEY.test(key) ? '[REDACTED]' : redactValue(read.value, depth + 1, seen);
76
+ out[key] = isSensitiveKey(key) ? '[REDACTED]' : redactValue(read.value, depth + 1, seen);
60
77
  }
61
78
  return out;
62
79
  }
@@ -75,7 +92,7 @@ export const redact = winston.format((info) => {
75
92
  ? '[Unreadable property]'
76
93
  : // A top-level primitive (e.g. token: "abc") has no children for redactValue to walk into —
77
94
  // the key itself has to be checked here too, not just inside the recursive object walk.
78
- SENSITIVE_KEY.test(key)
95
+ isSensitiveKey(key)
79
96
  ? '[REDACTED]'
80
97
  : redactValue(read.value);
81
98
  try {
@@ -116,7 +133,26 @@ function readOwnVersion(): string {
116
133
  const safeMessageString = (value: unknown): string =>
117
134
  typeof value === 'symbol' ? value.toString() : String(value);
118
135
 
119
- const textFormat = winston.format.printf((info) => `${info.timestamp} - ${info.level}: ${safeMessageString(info.message)}`);
136
+ // A failed ADO request's description (helpers/requestContext.ts), attached to the thrown error
137
+ // and merged onto the record by winston. In text mode — the default, which prints only the
138
+ // message — this is the only way the URL reaches stdout for a record whose own message doesn't
139
+ // carry it. Never throws: this runs inside the formatter, which has no try/catch around it.
140
+ const requestSuffix = (ctx: unknown): string => {
141
+ try {
142
+ if (!ctx || typeof ctx !== 'object') return '';
143
+ const c = ctx as Record<string, unknown>;
144
+ if (typeof c.url !== 'string') return '';
145
+ const method = typeof c.method === 'string' ? `${c.method} ` : '';
146
+ const status = typeof c.status === 'number' ? ` -> ${c.status}` : '';
147
+ return ` [${method}${c.url}${status}]`;
148
+ } catch {
149
+ return '';
150
+ }
151
+ };
152
+
153
+ const textFormat = winston.format.printf(
154
+ (info) => `${info.timestamp} - ${info.level}: ${safeMessageString(info.message)}${requestSuffix(info.adoRequest)}`
155
+ );
120
156
 
121
157
  // Bounded so one oversized message/stack can't produce an unbounded LogEvent document.
122
158
  const MAX_MESSAGE_LEN = 2000;
@@ -126,6 +162,26 @@ function clamp(value: unknown, max: number): string | undefined {
126
162
  return value.length > max ? value.slice(0, max) : value;
127
163
  }
128
164
 
165
+ // Re-validates the request description at the transport instead of trusting its shape — it
166
+ // arrives via an Error's own properties, which anything upstream could have set. Allowlisted
167
+ // keys only, each type-checked and bounded.
168
+ function pickContext(raw: unknown): DiagnosticEvent['context'] {
169
+ if (!raw || typeof raw !== 'object') return undefined;
170
+ const c = raw as Record<string, unknown>;
171
+ const context: NonNullable<DiagnosticEvent['context']> = {};
172
+ const method = clamp(c.method, CONTEXT_LIMITS.method);
173
+ const url = clamp(c.url, CONTEXT_LIMITS.url);
174
+ const requestBody = clamp(c.requestBody, CONTEXT_LIMITS.requestBody);
175
+ const responseExcerpt = clamp(c.responseExcerpt, CONTEXT_LIMITS.responseExcerpt);
176
+ if (method) context.method = method;
177
+ if (url) context.url = url;
178
+ if (requestBody) context.requestBody = requestBody;
179
+ if (responseExcerpt) context.responseExcerpt = responseExcerpt;
180
+ if (typeof c.status === 'number' && Number.isFinite(c.status)) context.status = c.status;
181
+ if (typeof c.attempt === 'number' && Number.isFinite(c.attempt)) context.attempt = c.attempt;
182
+ return Object.keys(context).length ? context : undefined;
183
+ }
184
+
129
185
  // Ships warn/error records to whatever LogSink the host process (docgen-content-control's
130
186
  // index.ts) has installed — this package never installs one itself, only reads it. This is
131
187
  // the dominant source of real ADO error records (tfs.ts's executeWithRetry is the single
@@ -177,11 +233,14 @@ export class DiagnosticsTransport extends Transport {
177
233
  message,
178
234
  err: hasErr
179
235
  ? {
180
- message,
236
+ // The error's own message when the call site supplied it (describeError); else
237
+ // the combined winston message, which is all a bare logger.error(err) leaves.
238
+ message: clamp(info.errMessage, MAX_MESSAGE_LEN) ?? message,
181
239
  code: typeof info.code === 'string' ? info.code : undefined,
182
240
  stack: clamp(info.stack, MAX_STACK_LEN),
183
241
  }
184
242
  : undefined,
243
+ context: pickContext(info.adoRequest),
185
244
  retainPending: !isWarnOrError && captureMode === 'retain-on-failure' ? true : undefined,
186
245
  };
187
246
  getLogSink()?.push(event);
@@ -208,11 +267,29 @@ const useJson = (process.env.LOG_FORMAT || 'text').toLowerCase() === 'json';
208
267
  // for free; info/warn/error volume (Phase 4's focus) is unaffected either way.
209
268
  const CONSOLE_LOG_LEVEL = process.env.LOG_LEVEL || 'info';
210
269
 
270
+ // Drops a record before the (comparatively expensive) redact/splat/format work when nothing
271
+ // would consume it: stdout wouldn't print it, and DiagnosticsTransport wouldn't persist it
272
+ // (only debug/info under a verbose/retain-on-failure run are captured below warn). The logger's
273
+ // own level gate must stay 'debug' for that capture, so this is the cheap early exit instead.
274
+ const LEVEL_RANK: Record<string, number> = { error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6 };
275
+ export const skipUncaptured = winston.format((info) => {
276
+ const printRank = LEVEL_RANK[CONSOLE_LOG_LEVEL];
277
+ const rank = LEVEL_RANK[String(info.level)];
278
+ if (printRank === undefined || rank === undefined || rank <= printRank) return info;
279
+ const mode = runContextStore.getStore()?.captureMode;
280
+ const captured =
281
+ DIAGNOSTICS_CAPTURE_ENABLED &&
282
+ (info.level === 'debug' || info.level === 'info') &&
283
+ (mode === 'verbose' || mode === 'retain-on-failure');
284
+ return captured ? info : false;
285
+ });
286
+
211
287
  logger = winston.createLogger({
212
288
  level: 'debug',
213
289
  defaultMeta: { service: '@elisra-devops/docgen-data-provider', version: readOwnVersion() },
214
290
  format: useJson
215
291
  ? winston.format.combine(
292
+ skipUncaptured(),
216
293
  winston.format.errors({ stack: true }),
217
294
  winston.format.timestamp(),
218
295
  withRunContext(),
@@ -221,6 +298,7 @@ logger = winston.createLogger({
221
298
  winston.format.json()
222
299
  )
223
300
  : winston.format.combine(
301
+ skipUncaptured(),
224
302
  winston.format.errors({ stack: true }),
225
303
  winston.format.timestamp(),
226
304
  withRunContext(),