@elisra-devops/docgen-data-provider 1.139.0 → 1.141.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/tfs.js +21 -14
- package/bin/helpers/tfs.js.map +1 -1
- package/bin/modules/JfrogDataProvider.js +1 -1
- package/bin/modules/JfrogDataProvider.js.map +1 -1
- package/bin/modules/PipelinesDataProvider.js +1 -1
- package/bin/modules/PipelinesDataProvider.js.map +1 -1
- package/bin/modules/ResultDataProvider.js +21 -15
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/modules/TicketsDataProvider.d.ts +3 -3
- package/bin/modules/TicketsDataProvider.js +80 -54
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/modules/JfrogDataProvider.test.js +3 -1
- package/bin/tests/modules/JfrogDataProvider.test.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +6 -2
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/bin/tests/modules/ticketsDataProvider.historical.test.js +111 -42
- package/bin/tests/modules/ticketsDataProvider.historical.test.js.map +1 -1
- package/bin/tests/utils/logger.test.d.ts +1 -0
- package/bin/tests/utils/logger.test.js +238 -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 +25 -0
- package/bin/utils/logSink.js +16 -0
- package/bin/utils/logSink.js.map +1 -0
- package/bin/utils/logger.d.ts +6 -0
- package/bin/utils/logger.js +219 -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/tfs.ts +19 -15
- package/src/modules/JfrogDataProvider.ts +1 -1
- package/src/modules/PipelinesDataProvider.ts +1 -1
- package/src/modules/ResultDataProvider.ts +20 -15
- package/src/modules/TicketsDataProvider.ts +93 -74
- package/src/tests/modules/JfrogDataProvider.test.ts +3 -1
- package/src/tests/modules/ResultDataProvider.test.ts +6 -2
- package/src/tests/modules/ticketsDataProvider.historical.test.ts +128 -43
- package/src/tests/utils/logger.test.ts +270 -0
- package/src/tests/utils/runContext.test.ts +48 -0
- package/src/utils/logSink.ts +44 -0
- package/src/utils/logger.ts +229 -29
- package/src/utils/runContext.ts +25 -0
|
@@ -10,7 +10,7 @@ export default class TicketsDataProvider {
|
|
|
10
10
|
token: string;
|
|
11
11
|
queriesList: Array<any>;
|
|
12
12
|
private limit;
|
|
13
|
-
private
|
|
13
|
+
private historicalTestPhaseFieldByProject;
|
|
14
14
|
constructor(orgUrl: string, token: string);
|
|
15
15
|
/**
|
|
16
16
|
* Extracts the Azure DevOps project name from a WIQL URL.
|
|
@@ -235,8 +235,8 @@ export default class TicketsDataProvider {
|
|
|
235
235
|
private historicalErrorMessage;
|
|
236
236
|
private isHistoricalMissingWorkItemError;
|
|
237
237
|
private isHistoricalUnknownFieldError;
|
|
238
|
-
private
|
|
239
|
-
private
|
|
238
|
+
private resolveHistoricalTestPhaseField;
|
|
239
|
+
private discoverHistoricalTestPhaseField;
|
|
240
240
|
private fetchHistoricalFieldsBatch;
|
|
241
241
|
private fetchHistoricalWorkItemsBatch;
|
|
242
242
|
private executeHistoricalQueryAtAsOf;
|
|
@@ -46,9 +46,11 @@ class TicketsDataProvider {
|
|
|
46
46
|
this.token = '';
|
|
47
47
|
this.queriesList = new Array();
|
|
48
48
|
this.limit = pLimit(10);
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
49
|
+
// Resolved once per project via resolveHistoricalTestPhaseField, so a
|
|
50
|
+
// compare's baseline and compareTo snapshots (and repeated queries against
|
|
51
|
+
// the same provider instance) only pay the discovery call once. Value is
|
|
52
|
+
// the real reference name, or null if the project defines no such field.
|
|
53
|
+
this.historicalTestPhaseFieldByProject = new Map();
|
|
52
54
|
this.orgUrl = orgUrl;
|
|
53
55
|
this.token = token;
|
|
54
56
|
}
|
|
@@ -611,8 +613,7 @@ class TicketsDataProvider {
|
|
|
611
613
|
: null;
|
|
612
614
|
}
|
|
613
615
|
catch (err) {
|
|
614
|
-
logger_1.default.error(`Error occurred while constructing the customer query list
|
|
615
|
-
logger_1.default.error(`Error stack ${err.message}`);
|
|
616
|
+
logger_1.default.error(`Error occurred while constructing the customer query list with query ${JSON.stringify(rootQuery)}`, err);
|
|
616
617
|
return null;
|
|
617
618
|
}
|
|
618
619
|
}
|
|
@@ -1636,59 +1637,80 @@ class TicketsDataProvider {
|
|
|
1636
1637
|
const message = this.historicalErrorMessage(error);
|
|
1637
1638
|
return /TF51535|Cannot find field/i.test(message);
|
|
1638
1639
|
}
|
|
1639
|
-
//
|
|
1640
|
-
//
|
|
1641
|
-
//
|
|
1642
|
-
//
|
|
1643
|
-
//
|
|
1644
|
-
//
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1640
|
+
// Different networks define the "Test Phase" custom field under different
|
|
1641
|
+
// reference names (Elisra.TestPhase vs Custom.TestPhase, or something else
|
|
1642
|
+
// entirely on a third network) — hardcoding candidate names and guessing is
|
|
1643
|
+
// inherently fragile. Resolve the project's real field list once (Fields -
|
|
1644
|
+
// List: GET {org}/{project}/_apis/wit/fields, supported since old TFS API
|
|
1645
|
+
// versions) and use whichever reference name actually matches, by
|
|
1646
|
+
// reference name or by friendly name. Resolves to null if the project
|
|
1647
|
+
// defines no such field at all.
|
|
1648
|
+
//
|
|
1649
|
+
// Caches the in-flight promise, not just the resolved value: a compare's
|
|
1650
|
+
// baseline and compareTo snapshots run concurrently against the same
|
|
1651
|
+
// provider instance, and both would otherwise read the cache before either
|
|
1652
|
+
// had a value to store, issuing the discovery request twice.
|
|
1653
|
+
resolveHistoricalTestPhaseField(project, apiVersion) {
|
|
1654
|
+
const cached = this.historicalTestPhaseFieldByProject.get(project);
|
|
1655
|
+
if (cached)
|
|
1656
|
+
return cached;
|
|
1657
|
+
const promise = this.discoverHistoricalTestPhaseField(project, apiVersion);
|
|
1658
|
+
this.historicalTestPhaseFieldByProject.set(project, promise);
|
|
1659
|
+
return promise;
|
|
1660
|
+
}
|
|
1661
|
+
async discoverHistoricalTestPhaseField(project, apiVersion) {
|
|
1662
|
+
const fieldsUrl = this.appendApiVersion(`${this.orgUrl}${project}/_apis/wit/fields`, apiVersion);
|
|
1663
|
+
try {
|
|
1664
|
+
const response = await tfs_1.TFSServices.getItemContent(fieldsUrl, this.token);
|
|
1665
|
+
const fields = Array.isArray(response === null || response === void 0 ? void 0 : response.value) ? response.value : [];
|
|
1666
|
+
const match = fields.find((field) => HISTORICAL_OPTIONAL_WORK_ITEM_FIELDS.some((candidate) => String((field === null || field === void 0 ? void 0 : field.referenceName) || '').toLowerCase() === candidate.toLowerCase())) || fields.find((field) => /test\s*phase/i.test(String((field === null || field === void 0 ? void 0 : field.name) || '')));
|
|
1667
|
+
const resolved = (match === null || match === void 0 ? void 0 : match.referenceName) ? String(match.referenceName) : null;
|
|
1668
|
+
if (!resolved) {
|
|
1669
|
+
logger_1.default.warn(`[historical-workitems] no Test Phase field defined for project '${project}'`);
|
|
1670
|
+
}
|
|
1671
|
+
return resolved;
|
|
1672
|
+
}
|
|
1673
|
+
catch (error) {
|
|
1674
|
+
logger_1.default.warn(`[historical-workitems] could not list fields for project '${project}' to resolve the ` +
|
|
1675
|
+
`Test Phase field name, omitting it: ${this.historicalErrorMessage(error)}`);
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1651
1678
|
}
|
|
1652
|
-
|
|
1679
|
+
fetchHistoricalFieldsBatch(workItemsBatchUrl, idChunk, asOf, project, fieldsForRequest) {
|
|
1653
1680
|
return this.limit(() => tfs_1.TFSServices.getItemContent(workItemsBatchUrl, this.token, 'post', {
|
|
1654
1681
|
ids: idChunk,
|
|
1655
1682
|
asOf,
|
|
1656
1683
|
fields: fieldsForRequest,
|
|
1657
1684
|
errorPolicy: 'Omit',
|
|
1658
1685
|
})).catch((error) => {
|
|
1659
|
-
|
|
1686
|
+
// Defensive net: the resolved field name came from the project's own
|
|
1687
|
+
// field list, so this shouldn't fire, but if it somehow still does,
|
|
1688
|
+
// fall back to the required fields and poison the cache so later
|
|
1689
|
+
// chunks/queries for this project stop requesting it too.
|
|
1690
|
+
const hasOptionalField = HISTORICAL_OPTIONAL_WORK_ITEM_FIELDS.some((f) => fieldsForRequest.includes(f))
|
|
1691
|
+
|| fieldsForRequest.some((f) => !HISTORICAL_REQUIRED_WORK_ITEM_FIELDS.includes(f));
|
|
1692
|
+
if (!hasOptionalField || !this.isHistoricalUnknownFieldError(error)) {
|
|
1660
1693
|
throw error;
|
|
1661
1694
|
}
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
if (reducedFields.length === fieldsForRequest.length) {
|
|
1672
|
-
throw error;
|
|
1673
|
-
}
|
|
1674
|
-
// Concurrent chunks can each discover a different bad field before any
|
|
1675
|
-
// of them updates the shared override — intersect rather than
|
|
1676
|
-
// overwrite, so one chunk's retry never re-requests a field another
|
|
1677
|
-
// chunk already proved invalid for this project.
|
|
1678
|
-
this.historicalFieldsOverride = this.historicalFieldsOverride
|
|
1679
|
-
? this.historicalFieldsOverride.filter((field) => reducedFields.includes(field))
|
|
1680
|
-
: reducedFields;
|
|
1681
|
-
logger_1.default.warn(`[historical-workitems] field${unknownField ? ` '${unknownField}'` : '(s)'} not defined ` +
|
|
1682
|
-
`for this project; retrying workitemsbatch without it: ${this.historicalErrorMessage(error)}`);
|
|
1683
|
-
return this.fetchHistoricalFieldsBatchAttempt(workItemsBatchUrl, idChunk, asOf, reducedFields, attemptsRemaining - 1);
|
|
1695
|
+
logger_1.default.warn(`[historical-workitems] resolved Test Phase field was rejected by workitemsbatch for ` +
|
|
1696
|
+
`project '${project}'; retrying without it: ${this.historicalErrorMessage(error)}`);
|
|
1697
|
+
this.historicalTestPhaseFieldByProject.set(project, Promise.resolve(null));
|
|
1698
|
+
return this.limit(() => tfs_1.TFSServices.getItemContent(workItemsBatchUrl, this.token, 'post', {
|
|
1699
|
+
ids: idChunk,
|
|
1700
|
+
asOf,
|
|
1701
|
+
fields: HISTORICAL_REQUIRED_WORK_ITEM_FIELDS,
|
|
1702
|
+
errorPolicy: 'Omit',
|
|
1703
|
+
}));
|
|
1684
1704
|
});
|
|
1685
1705
|
}
|
|
1686
|
-
fetchHistoricalFieldsBatch(workItemsBatchUrl, idChunk, asOf) {
|
|
1687
|
-
const fieldsForRequest = this.historicalFieldsOverride || HISTORICAL_WORK_ITEM_FIELDS;
|
|
1688
|
-
return this.fetchHistoricalFieldsBatchAttempt(workItemsBatchUrl, idChunk, asOf, fieldsForRequest, HISTORICAL_OPTIONAL_WORK_ITEM_FIELDS.length);
|
|
1689
|
-
}
|
|
1690
1706
|
async fetchHistoricalWorkItemsBatch(project, ids, asOf, apiVersion) {
|
|
1691
1707
|
const workItemsBatchUrl = this.appendApiVersion(`${this.orgUrl}${project}/_apis/wit/workitemsbatch`, apiVersion);
|
|
1708
|
+
// Resolved once per project, before any chunk starts, so every chunk
|
|
1709
|
+
// shares the same field list — no per-request guessing or retry race.
|
|
1710
|
+
const testPhaseField = await this.resolveHistoricalTestPhaseField(project, apiVersion);
|
|
1711
|
+
const fieldsForRequest = testPhaseField
|
|
1712
|
+
? [...HISTORICAL_REQUIRED_WORK_ITEM_FIELDS, testPhaseField]
|
|
1713
|
+
: HISTORICAL_REQUIRED_WORK_ITEM_FIELDS;
|
|
1692
1714
|
const idChunks = this.chunkHistoricalWorkItemIds(ids);
|
|
1693
1715
|
try {
|
|
1694
1716
|
const chunkResults = await Promise.all(idChunks.map((idChunk) =>
|
|
@@ -1704,7 +1726,7 @@ class TicketsDataProvider {
|
|
|
1704
1726
|
// two units against the shared concurrency cap, not one — otherwise
|
|
1705
1727
|
// the real ceiling on simultaneous ADO calls would silently double.
|
|
1706
1728
|
Promise.all([
|
|
1707
|
-
this.fetchHistoricalFieldsBatch(workItemsBatchUrl, idChunk, asOf),
|
|
1729
|
+
this.fetchHistoricalFieldsBatch(workItemsBatchUrl, idChunk, asOf, project, fieldsForRequest),
|
|
1708
1730
|
this.limit(() => tfs_1.TFSServices.getItemContent(workItemsBatchUrl, this.token, 'post', {
|
|
1709
1731
|
ids: idChunk,
|
|
1710
1732
|
asOf,
|
|
@@ -2112,8 +2134,10 @@ class TicketsDataProvider {
|
|
|
2112
2134
|
res.push(...subRes.value);
|
|
2113
2135
|
}
|
|
2114
2136
|
catch (error) {
|
|
2115
|
-
|
|
2116
|
-
|
|
2137
|
+
// JSON.stringify(error) on an AxiosError calls its own .toJSON(), which serializes
|
|
2138
|
+
// config.auth.password / config.headers.Authorization — the PAT, in plaintext. Pass
|
|
2139
|
+
// the error as meta instead and let the logger's redact format scrub it.
|
|
2140
|
+
logger_1.default.error(`error populating workitems array`, error);
|
|
2117
2141
|
return [];
|
|
2118
2142
|
}
|
|
2119
2143
|
}
|
|
@@ -2124,8 +2148,10 @@ class TicketsDataProvider {
|
|
|
2124
2148
|
res.push(...subRes.value);
|
|
2125
2149
|
}
|
|
2126
2150
|
catch (error) {
|
|
2127
|
-
|
|
2128
|
-
|
|
2151
|
+
// JSON.stringify(error) on an AxiosError calls its own .toJSON(), which serializes
|
|
2152
|
+
// config.auth.password / config.headers.Authorization — the PAT, in plaintext. Pass
|
|
2153
|
+
// the error as meta instead and let the logger's redact format scrub it.
|
|
2154
|
+
logger_1.default.error(`error populating workitems array`, error);
|
|
2129
2155
|
return [];
|
|
2130
2156
|
}
|
|
2131
2157
|
}
|
|
@@ -2249,8 +2275,9 @@ class TicketsDataProvider {
|
|
|
2249
2275
|
return attachmentList;
|
|
2250
2276
|
}
|
|
2251
2277
|
catch (e) {
|
|
2252
|
-
|
|
2253
|
-
|
|
2278
|
+
// JSON.stringify(e) on an Error returns "{}" — own enumerable props only, message/stack
|
|
2279
|
+
// are non-enumerable — so this logged literally nothing useful. Pass it as meta instead.
|
|
2280
|
+
logger_1.default.error(`error fetching attachments for work item ${id}`, e);
|
|
2254
2281
|
return [];
|
|
2255
2282
|
}
|
|
2256
2283
|
}
|
|
@@ -2470,8 +2497,7 @@ class TicketsDataProvider {
|
|
|
2470
2497
|
return { tree1: tree1Node, tree2: tree2Node };
|
|
2471
2498
|
}
|
|
2472
2499
|
catch (err) {
|
|
2473
|
-
logger_1.default.error(`Error occurred while constructing the query list
|
|
2474
|
-
logger_1.default.error(`Error stack ${err.message}`);
|
|
2500
|
+
logger_1.default.error(`Error occurred while constructing the query list with query ${JSON.stringify(rootQuery)}`, err);
|
|
2475
2501
|
}
|
|
2476
2502
|
}
|
|
2477
2503
|
/**
|