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 +10 -0
- package/lib/commands/hardis/org/diagnose/legacyapi.d.ts +32 -8
- package/lib/commands/hardis/org/diagnose/legacyapi.js +272 -78
- package/lib/commands/hardis/org/diagnose/legacyapi.js.map +1 -1
- package/lib/common/utils/filesUtils.d.ts +6 -0
- package/lib/common/utils/filesUtils.js +1 -1
- package/lib/common/utils/filesUtils.js.map +1 -1
- package/lib/common/utils/index.js +5 -0
- package/lib/common/utils/index.js.map +1 -1
- package/oclif.lock +149 -149
- package/oclif.manifest.json +3894 -3894
- package/package.json +3 -3
- package/yarn.lock +149 -149
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
|
|
12
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
...this.legacyApiDescriptors[1].errors,
|
|
139
|
-
...this.legacyApiDescriptors[2].errors,
|
|
140
|
-
];
|
|
168
|
+
WebSocketClient.sendProgressEndMessage();
|
|
169
|
+
await this.flushDescriptorErrors();
|
|
141
170
|
// Display summary
|
|
142
|
-
uxLog("
|
|
143
|
-
|
|
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
|
|
174
|
+
const errorCount = descriptor.totalErrors;
|
|
175
|
+
const colorMethod = descriptor.severity === 'ERROR' && errorCount > 0
|
|
146
176
|
? c.red
|
|
147
|
-
: descriptor.severity === 'WARNING' &&
|
|
177
|
+
: descriptor.severity === 'WARNING' && errorCount > 0
|
|
148
178
|
? c.yellow
|
|
149
179
|
: c.green;
|
|
150
|
-
|
|
180
|
+
const line = colorMethod(`- ${descriptor.deprecationRelease} : ${c.bold(errorCount)}`);
|
|
181
|
+
logLines.push(line);
|
|
151
182
|
}
|
|
152
|
-
uxLog("
|
|
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
|
-
|
|
157
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
193
|
-
notifDetailText += `• ${descriptor.severity}: API version calls found in logs: ${descriptor.
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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 (
|
|
245
|
+
if (totalErrorsFound > 0) {
|
|
206
246
|
notifSeverity = 'error';
|
|
207
|
-
notifText = `${
|
|
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:
|
|
260
|
+
metric: totalErrorsFound,
|
|
221
261
|
legacyApiSummary: this.ipResultsSorted,
|
|
222
262
|
},
|
|
223
263
|
metrics: {
|
|
224
|
-
LegacyApiCalls:
|
|
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(
|
|
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(
|
|
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 =
|
|
261
|
-
const apiFamily = logEntry.API_FAMILY ||
|
|
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 (
|
|
264
|
-
legacyApiDescriptor
|
|
265
|
-
legacyApiDescriptor
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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(
|
|
301
|
-
|
|
302
|
-
|
|
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(
|
|
313
|
-
const
|
|
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
|
|
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
|
-
|
|
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(
|
|
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("
|
|
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
|