sfdx-hardis 6.15.1 → 6.16.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/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@
4
4
 
5
5
  Note: Can be used with `sfdx plugins:install sfdx-hardis@beta` and docker image `hardisgroupcom/sfdx-hardis@beta`
6
6
 
7
+ ## [6.16.0] 2025-12-14
8
+
9
+ - [hardis:org:diagnose:legacyapi](https://sfdx-hardis.cloudity.com/hardis/org/diagnose/legacyapi/) enhancements:
10
+ - Detect calls to API Login to anticipate their [deprecation in Summer 27](https://help.salesforce.com/s/articleView?id=005132110&type=1)
11
+ - Make the command more efficient when handling a high number of log files
12
+ - Api Versions 21 to 30 are now flagged as errors.
13
+ - Add new Grafana Dashboard "Search Salesforce Org by Org Identifier"
14
+ - Fix default ConnectedApp name if it contains multiple `_`
15
+ - Fix tsconfig & vscode settings to improve VsCode performances
16
+
7
17
  ## [6.15.1] 2025-12-10
8
18
 
9
19
  - [hardis:doc:project2markdown](https://sfdx-hardis.cloudity.com/hardis/doc/project2markdown/): Fix crash when generating documentation when a formula is just `true`
@@ -1,5 +1,16 @@
1
1
  import { SfCommand } from '@salesforce/sf-plugins-core';
2
2
  import { AnyJson } from '@salesforce/ts-types';
3
+ type LegacyApiDescriptor = {
4
+ apiFamily: string[];
5
+ minApiVersion: number;
6
+ maxApiVersion: number;
7
+ severity: 'ERROR' | 'WARNING' | 'INFO';
8
+ deprecationRelease: string;
9
+ errors: any[];
10
+ totalErrors: number;
11
+ ipCounts: Record<string, number>;
12
+ apiResources?: string[];
13
+ };
3
14
  export default class LegacyApi extends SfCommand<any> {
4
15
  static title: string;
5
16
  static description: string;
@@ -8,21 +19,34 @@ export default class LegacyApi extends SfCommand<any> {
8
19
  static requiresProject: boolean;
9
20
  protected debugMode: boolean;
10
21
  protected apexSCannerCodeUrl: string;
11
- protected legacyApiDescriptors: {
12
- apiFamily: string[];
13
- minApiVersion: number;
14
- maxApiVersion: number;
15
- severity: string;
16
- deprecationRelease: string;
17
- errors: any[];
18
- }[];
22
+ protected articleTextLegacyApi: string;
23
+ protected legacyApiDescriptors: LegacyApiDescriptor[];
19
24
  protected allErrors: any[];
20
25
  protected ipResultsSorted: any[];
21
26
  protected outputFile: any;
22
27
  protected outputFilesRes: any;
23
28
  private tempDir;
29
+ private csvHeaderWritten;
30
+ private csvColumns;
31
+ private csvPreviousChunkEndedWithNewline;
32
+ private totalCsvRows;
33
+ private readonly notificationSampleLimit;
34
+ private notificationSampleTruncated;
24
35
  run(): Promise<AnyJson>;
25
36
  private runJsForce;
37
+ private resetCsvState;
38
+ private getTotalErrors;
39
+ private captureNotificationSample;
40
+ private updateIpCounts;
41
+ private ensureCsvColumns;
42
+ private appendRowsToCsv;
43
+ private flushDescriptorErrors;
44
+ private finalizeCsvOutput;
26
45
  private collectDeprecatedApiCalls;
27
46
  private generateSummaryLog;
47
+ private parseApiVersion;
48
+ private matchesApiFamily;
49
+ private matchesApiVersion;
50
+ private matchesApiResource;
28
51
  }
52
+ export {};
@@ -12,7 +12,7 @@ import { getNotificationButtons, getOrgMarkdown, getSeverityIcon } from '../../.
12
12
  import { soqlQuery } from '../../../../common/utils/apiUtils.js';
13
13
  import { WebSocketClient } from '../../../../common/websocketClient.js';
14
14
  import { NotifProvider } from '../../../../common/notifProvider/index.js';
15
- import { generateCsvFile, generateReportPath } from '../../../../common/utils/filesUtils.js';
15
+ import { generateCsvFile, generateReportPath, createXlsxFromCsv } from '../../../../common/utils/filesUtils.js';
16
16
  import { CONSTANTS } from '../../../../config/index.js';
17
17
  import { FileDownloader } from '../../../../common/utils/fileDownloader.js';
18
18
  import { setConnectionVariables } from '../../../../common/utils/orgUtils.js';
@@ -67,6 +67,9 @@ This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/sales
67
67
  static requiresProject = false;
68
68
  debugMode = false;
69
69
  apexSCannerCodeUrl = 'https://raw.githubusercontent.com/pozil/legacy-api-scanner/main/legacy-api-scanner.apex';
70
+ articleTextLegacyApi = `See article to solve issue before it's too late:
71
+ • EN: https://nicolas.vuillamy.fr/handle-salesforce-api-versions-deprecation-like-a-pro-335065f52238
72
+ • FR: https://leblog.hardis-group.com/portfolio/versions-dapi-salesforce-decommissionnees-que-faire/`;
70
73
  legacyApiDescriptors = [
71
74
  {
72
75
  apiFamily: ['SOAP', 'REST', 'BULK_API'],
@@ -75,6 +78,8 @@ This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/sales
75
78
  severity: 'ERROR',
76
79
  deprecationRelease: 'Summer 21 - retirement of 1 to 6',
77
80
  errors: [],
81
+ totalErrors: 0,
82
+ ipCounts: {},
78
83
  },
79
84
  {
80
85
  apiFamily: ['SOAP', 'REST', 'BULK_API'],
@@ -83,14 +88,29 @@ This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/sales
83
88
  severity: 'ERROR',
84
89
  deprecationRelease: 'Summer 22 - retirement of 7 to 20',
85
90
  errors: [],
91
+ totalErrors: 0,
92
+ ipCounts: {},
86
93
  },
87
94
  {
88
95
  apiFamily: ['SOAP', 'REST', 'BULK_API'],
89
96
  minApiVersion: 21.0,
90
97
  maxApiVersion: 30.0,
91
- severity: 'WARNING',
98
+ severity: 'ERROR',
92
99
  deprecationRelease: 'Summer 25 - retirement of 21 to 30',
93
100
  errors: [],
101
+ totalErrors: 0,
102
+ ipCounts: {},
103
+ },
104
+ {
105
+ apiFamily: ['SOAP'],
106
+ minApiVersion: 0.0,
107
+ maxApiVersion: Number.POSITIVE_INFINITY,
108
+ severity: 'WARNING',
109
+ deprecationRelease: 'Summer 27 - retirement of SOAP login',
110
+ errors: [],
111
+ totalErrors: 0,
112
+ ipCounts: {},
113
+ apiResources: ['login'],
94
114
  },
95
115
  ];
96
116
  allErrors = [];
@@ -99,6 +119,12 @@ This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/sales
99
119
  outputFilesRes = {};
100
120
  /* jscpd:ignore-end */
101
121
  tempDir;
122
+ csvHeaderWritten = false;
123
+ csvColumns = null;
124
+ csvPreviousChunkEndedWithNewline = true;
125
+ totalCsvRows = 0;
126
+ notificationSampleLimit = 1000;
127
+ notificationSampleTruncated = false;
102
128
  async run() {
103
129
  const { flags } = await this.parse(LegacyApi);
104
130
  this.debugMode = flags.debug || false;
@@ -108,11 +134,14 @@ This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/sales
108
134
  async runJsForce(flags) {
109
135
  const eventType = flags.eventtype || 'ApiTotalUsage';
110
136
  const limit = flags.limit || 999;
111
- this.outputFile = flags.outputfile || null;
112
- const limitConstraint = limit ? ` LIMIT ${limit}` : '';
113
137
  const conn = flags['target-org'].getConnection();
138
+ this.outputFile = await generateReportPath('legacy-api-calls', flags.outputfile || null);
139
+ await fs.remove(this.outputFile).catch(() => undefined);
140
+ this.resetCsvState();
141
+ const limitConstraint = limit ? ` LIMIT ${limit}` : '';
114
142
  this.tempDir = await createTempDir();
115
143
  // Get EventLogFile records with EventType = 'ApiTotalUsage'
144
+ uxLog("action", this, c.cyan(`Querying org for EventLogFile entries of type ${eventType} to detect Legacy API calls...`));
116
145
  const logCountQuery = `SELECT COUNT() FROM EventLogFile WHERE EventType = '${eventType}'`;
117
146
  const logCountRes = await soqlQuery(logCountQuery, conn);
118
147
  if (logCountRes.totalSize === 0) {
@@ -129,37 +158,40 @@ This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/sales
129
158
  const logCollectQuery = `SELECT LogFile FROM EventLogFile WHERE EventType = '${eventType}' ORDER BY LogDate DESC` + limitConstraint;
130
159
  const eventLogRes = await soqlQuery(logCollectQuery, conn);
131
160
  // Collect legacy api calls from logs
132
- uxLog("action", this, c.cyan('Calling org API to get CSV content of each EventLogFile record, then parse and analyze it...'));
161
+ WebSocketClient.sendProgressStartMessage("Downloading and analyzing log files...", eventLogRes.records.length);
162
+ let counter = 0;
133
163
  for (const eventLogFile of eventLogRes.records) {
134
164
  await this.collectDeprecatedApiCalls(eventLogFile.LogFile, conn);
165
+ counter++;
166
+ WebSocketClient.sendProgressStepMessage(counter, eventLogRes.records.length);
135
167
  }
136
- this.allErrors = [
137
- ...this.legacyApiDescriptors[0].errors,
138
- ...this.legacyApiDescriptors[1].errors,
139
- ...this.legacyApiDescriptors[2].errors,
140
- ];
168
+ WebSocketClient.sendProgressEndMessage();
169
+ await this.flushDescriptorErrors();
141
170
  // Display summary
142
- uxLog("other", this, '');
143
- uxLog("action", this, c.cyan('Results:'));
171
+ uxLog("action", this, c.cyan('Results of Legacy API calls analysis:'));
172
+ const logLines = [];
144
173
  for (const descriptor of this.legacyApiDescriptors) {
145
- const colorMethod = descriptor.severity === 'ERROR' && descriptor.errors.length > 0
174
+ const errorCount = descriptor.totalErrors;
175
+ const colorMethod = descriptor.severity === 'ERROR' && errorCount > 0
146
176
  ? c.red
147
- : descriptor.severity === 'WARNING' && descriptor.errors.length > 0
177
+ : descriptor.severity === 'WARNING' && errorCount > 0
148
178
  ? c.yellow
149
179
  : c.green;
150
- uxLog("other", this, colorMethod(`- ${descriptor.deprecationRelease} : ${c.bold(descriptor.errors.length)}`));
180
+ const line = colorMethod(`- ${descriptor.deprecationRelease} : ${c.bold(errorCount)}`);
181
+ logLines.push(line);
151
182
  }
152
- uxLog("other", this, '');
183
+ uxLog("log", this, logLines.join('\n'));
153
184
  // Build command result
154
185
  let msg = 'No deprecated API call has been found in ApiTotalUsage logs';
155
186
  let statusCode = 0;
156
- if (this.legacyApiDescriptors.filter((descriptor) => descriptor.severity === 'ERROR' && descriptor.errors.length > 0)
157
- .length > 0) {
187
+ const hasBlockingErrors = this.legacyApiDescriptors.some((descriptor) => descriptor.severity === 'ERROR' && descriptor.totalErrors > 0);
188
+ const hasWarningsOnly = this.legacyApiDescriptors.some((descriptor) => descriptor.severity === 'WARNING' && descriptor.totalErrors > 0);
189
+ if (hasBlockingErrors) {
158
190
  msg = 'Found legacy API versions calls in logs';
159
191
  statusCode = 1;
160
192
  uxLog("error", this, c.red(c.bold(msg)));
161
193
  }
162
- else if (this.legacyApiDescriptors.filter((descriptor) => descriptor.severity === 'WARNING' && descriptor.errors.length > 0).length > 0) {
194
+ else if (hasWarningsOnly) {
163
195
  msg = 'Found deprecated API versions calls in logs that will not be supported anymore in the future';
164
196
  statusCode = 0;
165
197
  uxLog("warning", this, c.yellow(c.bold(msg)));
@@ -168,43 +200,51 @@ This command is part of [sfdx-hardis Monitoring](${CONSTANTS.DOC_URL_ROOT}/sales
168
200
  uxLog("success", this, c.green(msg));
169
201
  }
170
202
  // Generate main CSV file
171
- this.outputFile = await generateReportPath('legacy-api-calls', this.outputFile);
172
- this.outputFilesRes = await generateCsvFile(this.allErrors, this.outputFile, { fileTitle: 'Legacy API Calls' });
203
+ await this.finalizeCsvOutput();
173
204
  // Generate one summary file by severity
174
205
  const outputFileIps = [];
175
206
  for (const descriptor of this.legacyApiDescriptors) {
176
- const errors = descriptor.errors;
177
- if (errors.length > 0) {
178
- const outputFileIp = await this.generateSummaryLog(errors, descriptor.severity);
179
- outputFileIps.push(outputFileIp);
207
+ if (descriptor.totalErrors > 0) {
208
+ const outputFileIp = await this.generateSummaryLog(descriptor.ipCounts, descriptor.severity);
209
+ if (outputFileIp) {
210
+ outputFileIps.push(outputFileIp);
211
+ }
180
212
  // Trigger command to open CSV file in VS Code extension
181
- WebSocketClient.requestOpenFile(outputFileIp);
213
+ if (outputFileIp) {
214
+ WebSocketClient.requestOpenFile(outputFileIp);
215
+ }
182
216
  }
183
217
  }
184
218
  // Debug or manage CSV file generation error
185
219
  if (this.debugMode || this.outputFile == null) {
186
220
  for (const descriptor of this.legacyApiDescriptors) {
187
- uxLog("log", this, c.grey(`- ${descriptor.deprecationRelease} : ${JSON.stringify(descriptor.errors.length)}`));
221
+ uxLog("log", this, c.grey(`- ${descriptor.deprecationRelease} : ${JSON.stringify(descriptor.totalErrors)}`));
188
222
  }
189
223
  }
190
224
  let notifDetailText = '';
191
225
  for (const descriptor of this.legacyApiDescriptors) {
192
- if (descriptor.errors.length > 0) {
193
- notifDetailText += `• ${descriptor.severity}: API version calls found in logs: ${descriptor.errors.length} (${descriptor.deprecationRelease})\n`;
226
+ if (descriptor.totalErrors > 0) {
227
+ notifDetailText += `• ${descriptor.severity}: API version calls found in logs: ${descriptor.totalErrors} (${descriptor.deprecationRelease})\n`;
194
228
  }
195
229
  }
196
- notifDetailText += `
197
- See article to solve issue before it's too late:
198
- • EN: https://nicolas.vuillamy.fr/handle-salesforce-api-versions-deprecation-like-a-pro-335065f52238
199
- • FR: https://leblog.hardis-group.com/portfolio/versions-dapi-salesforce-decommissionnees-que-faire/`;
230
+ notifDetailText += "\n" + this.articleTextLegacyApi;
231
+ if (WebSocketClient.isAliveWithLwcUI()) {
232
+ WebSocketClient.sendReportFileMessage("https://nicolas.vuillamy.fr/handle-salesforce-api-versions-deprecation-like-a-pro-335065f52238", "Article (EN)", 'docUrl');
233
+ WebSocketClient.sendReportFileMessage("https://leblog.hardis-group.com/portfolio/versions-dapi-salesforce-decommissionnees-que-faire/", "Article (FR)", 'docUrl');
234
+ }
235
+ if (this.notificationSampleTruncated) {
236
+ notifDetailText += `
237
+ Only the first ${this.notificationSampleLimit} log entries are attached to this notification.`;
238
+ }
200
239
  // Build notifications
201
240
  const orgMarkdown = await getOrgMarkdown(flags['target-org']?.getConnection()?.instanceUrl);
202
241
  const notifButtons = await getNotificationButtons();
242
+ const totalErrorsFound = this.getTotalErrors();
203
243
  let notifSeverity = 'log';
204
244
  let notifText = `No deprecated Salesforce API versions are used in ${orgMarkdown}`;
205
- if (this.allErrors.length > 0) {
245
+ if (totalErrorsFound > 0) {
206
246
  notifSeverity = 'error';
207
- notifText = `${this.allErrors.length} deprecated Salesforce API versions are used in ${orgMarkdown}`;
247
+ notifText = `${totalErrorsFound} deprecated Salesforce API versions are used in ${orgMarkdown}`;
208
248
  }
209
249
  // Post notifications
210
250
  await setConnectionVariables(flags['target-org']?.getConnection()); // Required for some notifications providers like Email
@@ -217,13 +257,14 @@ See article to solve issue before it's too late:
217
257
  attachedFiles: this.outputFilesRes.xlsxFile ? [this.outputFilesRes.xlsxFile, this.outputFilesRes.xlsxFile2] : [],
218
258
  logElements: this.allErrors,
219
259
  data: {
220
- metric: this.allErrors.length,
260
+ metric: totalErrorsFound,
221
261
  legacyApiSummary: this.ipResultsSorted,
222
262
  },
223
263
  metrics: {
224
- LegacyApiCalls: this.allErrors.length,
264
+ LegacyApiCalls: totalErrorsFound,
225
265
  },
226
266
  });
267
+ uxLog("log", this, c.grey(notifDetailText));
227
268
  if ((this.argv || []).includes('legacyapi')) {
228
269
  process.exitCode = statusCode;
229
270
  }
@@ -236,6 +277,126 @@ See article to solve issue before it's too late:
236
277
  legacyApiResults: this.legacyApiDescriptors,
237
278
  };
238
279
  }
280
+ resetCsvState() {
281
+ this.csvHeaderWritten = false;
282
+ this.csvColumns = null;
283
+ this.csvPreviousChunkEndedWithNewline = true;
284
+ this.totalCsvRows = 0;
285
+ this.allErrors = [];
286
+ this.ipResultsSorted = [];
287
+ this.notificationSampleTruncated = false;
288
+ this.outputFilesRes = {};
289
+ for (const descriptor of this.legacyApiDescriptors) {
290
+ descriptor.errors = [];
291
+ descriptor.totalErrors = 0;
292
+ descriptor.ipCounts = {};
293
+ }
294
+ }
295
+ getTotalErrors() {
296
+ return this.legacyApiDescriptors.reduce((sum, descriptor) => sum + descriptor.totalErrors, 0);
297
+ }
298
+ captureNotificationSample(entries) {
299
+ if (!entries || entries.length === 0) {
300
+ return;
301
+ }
302
+ for (const entry of entries) {
303
+ if (this.allErrors.length < this.notificationSampleLimit) {
304
+ this.allErrors.push(entry);
305
+ }
306
+ else {
307
+ this.notificationSampleTruncated = true;
308
+ break;
309
+ }
310
+ }
311
+ }
312
+ updateIpCounts(descriptor, errors) {
313
+ if (!errors || errors.length === 0) {
314
+ return;
315
+ }
316
+ for (const eventLogRecord of errors) {
317
+ if (!eventLogRecord || !eventLogRecord.CLIENT_IP) {
318
+ continue;
319
+ }
320
+ descriptor.ipCounts[eventLogRecord.CLIENT_IP] = (descriptor.ipCounts[eventLogRecord.CLIENT_IP] || 0) + 1;
321
+ }
322
+ }
323
+ ensureCsvColumns(rows) {
324
+ if (this.csvColumns && this.csvColumns.length > 0) {
325
+ return;
326
+ }
327
+ const columnSet = new Set();
328
+ for (const row of rows) {
329
+ if (!row) {
330
+ continue;
331
+ }
332
+ Object.keys(row).forEach((key) => columnSet.add(key));
333
+ }
334
+ this.csvColumns = Array.from(columnSet);
335
+ }
336
+ async appendRowsToCsv(rows) {
337
+ if (!rows || rows.length === 0) {
338
+ return;
339
+ }
340
+ if (!this.outputFile) {
341
+ throw new Error('Output file path is not initialized');
342
+ }
343
+ this.ensureCsvColumns(rows);
344
+ if (!this.csvColumns || this.csvColumns.length === 0) {
345
+ return;
346
+ }
347
+ const csvString = Papa.unparse(rows, {
348
+ header: !this.csvHeaderWritten,
349
+ columns: this.csvColumns,
350
+ });
351
+ if (!this.csvHeaderWritten) {
352
+ await fs.writeFile(this.outputFile, csvString, 'utf8');
353
+ this.csvHeaderWritten = true;
354
+ }
355
+ else if (csvString.length > 0) {
356
+ const prefix = this.csvPreviousChunkEndedWithNewline ? '' : '\n';
357
+ await fs.appendFile(this.outputFile, prefix + csvString, 'utf8');
358
+ }
359
+ this.csvPreviousChunkEndedWithNewline = csvString.endsWith('\n');
360
+ this.totalCsvRows += rows.length;
361
+ }
362
+ async flushDescriptorErrors() {
363
+ for (const descriptor of this.legacyApiDescriptors) {
364
+ if (!descriptor.errors || descriptor.errors.length === 0) {
365
+ continue;
366
+ }
367
+ const descriptorErrors = descriptor.errors;
368
+ descriptor.totalErrors += descriptorErrors.length;
369
+ this.captureNotificationSample(descriptorErrors);
370
+ this.updateIpCounts(descriptor, descriptorErrors);
371
+ await this.appendRowsToCsv(descriptorErrors);
372
+ descriptor.errors = [];
373
+ }
374
+ }
375
+ async finalizeCsvOutput() {
376
+ if (!this.outputFile) {
377
+ return;
378
+ }
379
+ if (!(await fs.pathExists(this.outputFile))) {
380
+ await fs.ensureDir(path.dirname(this.outputFile));
381
+ await fs.writeFile(this.outputFile, '', 'utf8');
382
+ }
383
+ uxLog("action", this, c.cyan(c.italic(`Please see detailed CSV log in ${c.bold(this.outputFile)}`)));
384
+ this.outputFilesRes.csvFile = this.outputFile;
385
+ if (!WebSocketClient.isAliveWithLwcUI()) {
386
+ WebSocketClient.requestOpenFile(this.outputFile);
387
+ }
388
+ WebSocketClient.sendReportFileMessage(this.outputFile, 'Legacy API Calls (CSV)', 'report');
389
+ if (this.totalCsvRows > 0) {
390
+ const result = {};
391
+ await createXlsxFromCsv(this.outputFile, { fileTitle: 'Legacy API Calls' }, result);
392
+ if (result.xlsxFile) {
393
+ this.outputFilesRes.xlsxFile = result.xlsxFile;
394
+ }
395
+ }
396
+ else {
397
+ uxLog("other", this, c.grey(`No XLS file generated as ${this.outputFile} is empty`));
398
+ }
399
+ }
239
400
  // GET csv log file and check for legacy API calls within
240
401
  async collectDeprecatedApiCalls(logFileUrl, conn) {
241
402
  // Load icons
@@ -243,12 +404,12 @@ See article to solve issue before it's too late:
243
404
  const severityIconWarning = getSeverityIcon('warning');
244
405
  const severityIconInfo = getSeverityIcon('info');
245
406
  // Download file as stream, and process chuck by chuck
246
- uxLog("log", this, c.grey(`- processing ${logFileUrl}...`));
407
+ uxLog("log", this, c.grey(`Downloading ${logFileUrl}...`));
247
408
  const fetchUrl = `${conn.instanceUrl}${logFileUrl}`;
248
409
  const outputFile = path.join(this.tempDir, Math.random().toString(36).substring(7) + ".csv");
249
410
  const downloadResult = await new FileDownloader(fetchUrl, { conn: conn, outputFile: outputFile }).download();
250
411
  if (downloadResult.success) {
251
- uxLog("log", this, c.grey(`-- parsing downloaded CSV from ${outputFile} and check for deprecated calls...`));
412
+ uxLog("log", this, c.grey(`Parsing downloaded CSV from ${outputFile} and check for deprecated calls...`));
252
413
  const outputFileStream = fs.createReadStream(outputFile, { encoding: 'utf8' });
253
414
  await new Promise((resolve, reject) => {
254
415
  Papa.parse(outputFileStream, {
@@ -257,30 +418,32 @@ See article to solve issue before it's too late:
257
418
  chunk: (results) => {
258
419
  // Look in check the entries that match a deprecation description
259
420
  for (const logEntry of results.data) {
260
- const apiVersion = logEntry.API_VERSION ? parseFloat(logEntry.API_VERSION) : parseFloat('999.0');
261
- const apiFamily = logEntry.API_FAMILY || null;
421
+ const apiVersion = this.parseApiVersion(logEntry.API_VERSION);
422
+ const apiFamily = (logEntry.API_FAMILY || '').toUpperCase();
423
+ const apiResource = (logEntry.API_RESOURCE || '').toLowerCase();
262
424
  for (const legacyApiDescriptor of this.legacyApiDescriptors) {
263
- if (legacyApiDescriptor.apiFamily.includes(apiFamily) &&
264
- legacyApiDescriptor.minApiVersion <= apiVersion &&
265
- legacyApiDescriptor.maxApiVersion >= apiVersion) {
266
- logEntry.SFDX_HARDIS_DEPRECATION_RELEASE = legacyApiDescriptor.deprecationRelease;
267
- logEntry.SFDX_HARDIS_SEVERITY = legacyApiDescriptor.severity;
268
- if (legacyApiDescriptor.severity === 'ERROR') {
269
- logEntry.severity = 'error';
270
- logEntry.severityIcon = severityIconError;
271
- }
272
- else if (legacyApiDescriptor.severity === 'WARNING') {
273
- logEntry.severity = 'warning';
274
- logEntry.severityIcon = severityIconWarning;
275
- }
276
- else {
277
- // severity === 'INFO'
278
- logEntry.severity = 'info';
279
- logEntry.severityIcon = severityIconInfo;
280
- }
281
- legacyApiDescriptor.errors.push(logEntry);
282
- break;
425
+ if (!this.matchesApiFamily(legacyApiDescriptor, apiFamily) ||
426
+ !this.matchesApiVersion(legacyApiDescriptor, apiVersion) ||
427
+ !this.matchesApiResource(legacyApiDescriptor, apiResource)) {
428
+ continue;
429
+ }
430
+ logEntry.SFDX_HARDIS_DEPRECATION_RELEASE = legacyApiDescriptor.deprecationRelease;
431
+ logEntry.SFDX_HARDIS_SEVERITY = legacyApiDescriptor.severity;
432
+ if (legacyApiDescriptor.severity === 'ERROR') {
433
+ logEntry.severity = 'error';
434
+ logEntry.severityIcon = severityIconError;
435
+ }
436
+ else if (legacyApiDescriptor.severity === 'WARNING') {
437
+ logEntry.severity = 'warning';
438
+ logEntry.severityIcon = severityIconWarning;
283
439
  }
440
+ else {
441
+ // severity === 'INFO'
442
+ logEntry.severity = 'info';
443
+ logEntry.severityIcon = severityIconInfo;
444
+ }
445
+ legacyApiDescriptor.errors.push(logEntry);
446
+ break;
284
447
  }
285
448
  }
286
449
  },
@@ -292,50 +455,81 @@ See article to solve issue before it's too late:
292
455
  },
293
456
  });
294
457
  });
458
+ await this.flushDescriptorErrors();
459
+ await fs.remove(outputFile).catch(() => undefined);
295
460
  }
296
461
  else {
297
462
  uxLog("warning", this, c.yellow(`Warning: Unable to process logs of ${logFileUrl}`));
298
463
  }
299
464
  }
300
- async generateSummaryLog(errors, severity) {
301
- // Collect all ips and the number of calls
302
- const ipList = {};
303
- for (const eventLogRecord of errors) {
304
- if (eventLogRecord.CLIENT_IP) {
305
- const ipInfo = ipList[eventLogRecord.CLIENT_IP] || { count: 0 };
306
- ipInfo.count++;
307
- ipList[eventLogRecord.CLIENT_IP] = ipInfo;
308
- }
465
+ async generateSummaryLog(ipCounts, severity) {
466
+ if (!ipCounts || Object.keys(ipCounts).length === 0) {
467
+ return null;
309
468
  }
310
469
  // Try to get hostname for ips
311
470
  const ipResults = [];
312
- for (const ip of Object.keys(ipList)) {
313
- const ipInfo = ipList[ip];
314
- let hostname;
471
+ for (const ip of Object.keys(ipCounts)) {
472
+ const count = ipCounts[ip];
473
+ let hostname = 'unknown';
315
474
  try {
316
475
  hostname = await dnsPromises.reverse(ip);
317
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
318
476
  }
319
477
  catch (e) {
320
478
  hostname = 'unknown';
479
+ uxLog("other", this, c.grey(`Unable to resolve hostname for IP ${ip}: ${e}`));
321
480
  }
322
- const ipResult = { CLIENT_IP: ip, CLIENT_HOSTNAME: hostname, SFDX_HARDIS_COUNT: ipInfo.count };
481
+ const formattedHostname = Array.isArray(hostname) ? hostname.join(', ') : hostname;
482
+ const ipResult = { CLIENT_IP: ip, CLIENT_HOSTNAME: formattedHostname, SFDX_HARDIS_COUNT: count };
323
483
  ipResults.push(ipResult);
324
484
  }
325
- this.ipResultsSorted = sortArray(ipResults, {
485
+ const sortedIpResults = sortArray(ipResults, {
326
486
  by: ['SFDX_HARDIS_COUNT'],
327
487
  order: ['desc'],
328
488
  });
489
+ this.ipResultsSorted = [
490
+ ...this.ipResultsSorted,
491
+ ...sortedIpResults.map((entry) => ({ ...entry, severity })),
492
+ ];
329
493
  // Write output CSV with client api info
330
494
  const outputFileIps = this.outputFile.endsWith('.csv')
331
495
  ? this.outputFile.replace('.csv', '.api-clients-' + severity + '.csv')
332
496
  : this.outputFile + 'api-clients-' + severity + '.csv';
333
- const outputFileIpsRes = await generateCsvFile(this.ipResultsSorted, outputFileIps, { fileTitle: `Legacy API Clients - ${severity}` });
497
+ const outputFileIpsRes = await generateCsvFile(sortedIpResults, outputFileIps, {
498
+ fileTitle: `Legacy API Clients - ${severity}`,
499
+ });
334
500
  if (outputFileIpsRes.xlsxFile) {
335
501
  this.outputFilesRes.xlsxFile2 = outputFileIpsRes.xlsxFile;
336
502
  }
337
- uxLog("other", this, c.italic(c.cyan(`Please see info about ${severity} API callers in ${c.bold(outputFileIps)}`)));
503
+ uxLog("log", this, c.italic(c.cyan(`Please see info about ${severity} API callers in ${c.bold(outputFileIps)}`)));
338
504
  return outputFileIps;
339
505
  }
506
+ parseApiVersion(rawValue) {
507
+ if (rawValue === undefined || rawValue === null || rawValue === '') {
508
+ return Number.POSITIVE_INFINITY;
509
+ }
510
+ if (typeof rawValue === 'number') {
511
+ return Number.isFinite(rawValue) ? rawValue : Number.POSITIVE_INFINITY;
512
+ }
513
+ const parsed = parseFloat(rawValue);
514
+ return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
515
+ }
516
+ matchesApiFamily(descriptor, apiFamily) {
517
+ if (!apiFamily) {
518
+ return false;
519
+ }
520
+ return descriptor.apiFamily.some((family) => family.toUpperCase() === apiFamily);
521
+ }
522
+ matchesApiVersion(descriptor, apiVersion) {
523
+ return apiVersion >= descriptor.minApiVersion && apiVersion <= descriptor.maxApiVersion;
524
+ }
525
+ matchesApiResource(descriptor, apiResource) {
526
+ if (!descriptor.apiResources || descriptor.apiResources.length === 0) {
527
+ return true;
528
+ }
529
+ if (!apiResource) {
530
+ return false;
531
+ }
532
+ return descriptor.apiResources.some((resource) => resource.toLowerCase() === apiResource);
533
+ }
340
534
  }
341
535
  //# sourceMappingURL=legacyapi.js.map