@datadog/datadog-ci 2.17.2 → 2.18.1

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 (100) hide show
  1. package/dist/commands/cloud-run/cli.d.ts +1 -0
  2. package/dist/commands/cloud-run/cli.js +5 -0
  3. package/dist/commands/cloud-run/cli.js.map +1 -0
  4. package/dist/commands/cloud-run/constants.d.ts +1 -0
  5. package/dist/commands/cloud-run/constants.js +22 -0
  6. package/dist/commands/cloud-run/constants.js.map +1 -0
  7. package/dist/commands/cloud-run/flare.d.ts +60 -0
  8. package/dist/commands/cloud-run/flare.js +369 -0
  9. package/dist/commands/cloud-run/flare.js.map +1 -0
  10. package/dist/commands/cloud-run/interfaces.d.ts +27 -0
  11. package/dist/commands/cloud-run/interfaces.js +3 -0
  12. package/dist/commands/cloud-run/interfaces.js.map +1 -0
  13. package/dist/commands/cloud-run/renderer.d.ts +4 -0
  14. package/dist/commands/cloud-run/renderer.js +38 -0
  15. package/dist/commands/cloud-run/renderer.js.map +1 -0
  16. package/dist/commands/flutter-symbols/renderer.js +2 -0
  17. package/dist/commands/flutter-symbols/renderer.js.map +1 -1
  18. package/dist/commands/gate/api.js +4 -0
  19. package/dist/commands/gate/api.js.map +1 -1
  20. package/dist/commands/gate/evaluate.js +5 -4
  21. package/dist/commands/gate/evaluate.js.map +1 -1
  22. package/dist/commands/gate/interfaces.d.ts +5 -0
  23. package/dist/commands/gate/renderer.d.ts +1 -1
  24. package/dist/commands/gate/renderer.js +16 -2
  25. package/dist/commands/gate/renderer.js.map +1 -1
  26. package/dist/commands/junit/utils.js +6 -13
  27. package/dist/commands/junit/utils.js.map +1 -1
  28. package/dist/commands/lambda/constants.d.ts +11 -8
  29. package/dist/commands/lambda/constants.js +85 -15
  30. package/dist/commands/lambda/constants.js.map +1 -1
  31. package/dist/commands/lambda/flare.d.ts +26 -40
  32. package/dist/commands/lambda/flare.js +275 -202
  33. package/dist/commands/lambda/flare.js.map +1 -1
  34. package/dist/commands/lambda/functions/commons.d.ts +7 -13
  35. package/dist/commands/lambda/functions/commons.js +39 -56
  36. package/dist/commands/lambda/functions/commons.js.map +1 -1
  37. package/dist/commands/lambda/functions/instrument.js +46 -45
  38. package/dist/commands/lambda/functions/instrument.js.map +1 -1
  39. package/dist/commands/lambda/functions/uninstrument.js +29 -28
  40. package/dist/commands/lambda/functions/uninstrument.js.map +1 -1
  41. package/dist/commands/lambda/instrument.js +20 -17
  42. package/dist/commands/lambda/instrument.js.map +1 -1
  43. package/dist/commands/lambda/prompt.d.ts +1 -2
  44. package/dist/commands/lambda/prompt.js +32 -32
  45. package/dist/commands/lambda/prompt.js.map +1 -1
  46. package/dist/commands/lambda/renderers/common-renderer.d.ts +0 -26
  47. package/dist/commands/lambda/renderers/common-renderer.js +4 -32
  48. package/dist/commands/lambda/renderers/common-renderer.js.map +1 -1
  49. package/dist/commands/lambda/renderers/instrument-uninstrument-renderer.js +27 -27
  50. package/dist/commands/lambda/renderers/instrument-uninstrument-renderer.js.map +1 -1
  51. package/dist/commands/lambda/uninstrument.js +10 -8
  52. package/dist/commands/lambda/uninstrument.js.map +1 -1
  53. package/dist/commands/sarif/api.js +2 -18
  54. package/dist/commands/sarif/api.js.map +1 -1
  55. package/dist/commands/stepfunctions/awsCommands.d.ts +2 -0
  56. package/dist/commands/stepfunctions/awsCommands.js +26 -2
  57. package/dist/commands/stepfunctions/awsCommands.js.map +1 -1
  58. package/dist/commands/stepfunctions/constants.d.ts +1 -0
  59. package/dist/commands/stepfunctions/constants.js +2 -1
  60. package/dist/commands/stepfunctions/constants.js.map +1 -1
  61. package/dist/commands/stepfunctions/helpers.d.ts +22 -0
  62. package/dist/commands/stepfunctions/helpers.js +62 -4
  63. package/dist/commands/stepfunctions/helpers.js.map +1 -1
  64. package/dist/commands/stepfunctions/instrument.d.ts +1 -0
  65. package/dist/commands/stepfunctions/instrument.js +15 -4
  66. package/dist/commands/stepfunctions/instrument.js.map +1 -1
  67. package/dist/commands/synthetics/interfaces.d.ts +2 -1
  68. package/dist/commands/synthetics/interfaces.js.map +1 -1
  69. package/dist/commands/synthetics/reporters/default.js +3 -5
  70. package/dist/commands/synthetics/reporters/default.js.map +1 -1
  71. package/dist/commands/synthetics/utils.js +3 -9
  72. package/dist/commands/synthetics/utils.js.map +1 -1
  73. package/dist/constants.d.ts +10 -0
  74. package/dist/constants.js +14 -1
  75. package/dist/constants.js.map +1 -1
  76. package/dist/helpers/app.d.ts +2 -0
  77. package/dist/helpers/app.js +17 -0
  78. package/dist/helpers/app.js.map +1 -0
  79. package/dist/helpers/flare.d.ts +19 -0
  80. package/dist/helpers/flare.js +81 -0
  81. package/dist/helpers/flare.js.map +1 -0
  82. package/dist/helpers/fs.d.ts +31 -0
  83. package/dist/helpers/fs.js +117 -0
  84. package/dist/helpers/fs.js.map +1 -0
  85. package/dist/helpers/prompt.d.ts +6 -0
  86. package/dist/helpers/prompt.js +39 -0
  87. package/dist/helpers/prompt.js.map +1 -0
  88. package/dist/helpers/renderer.d.ts +36 -0
  89. package/dist/helpers/renderer.js +47 -0
  90. package/dist/helpers/renderer.js.map +1 -0
  91. package/dist/helpers/utils.d.ts +8 -0
  92. package/dist/helpers/utils.js +40 -1
  93. package/dist/helpers/utils.js.map +1 -1
  94. package/package.json +8 -2
  95. package/dist/commands/lambda/renderers/flare-renderer.d.ts +0 -9
  96. package/dist/commands/lambda/renderers/flare-renderer.js +0 -18
  97. package/dist/commands/lambda/renderers/flare-renderer.js.map +0 -1
  98. package/dist/helpers/file.d.ts +0 -1
  99. package/dist/helpers/file.js +0 -9
  100. package/dist/helpers/file.js.map +0 -1
@@ -31,31 +31,31 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
31
31
  return (mod && mod.__esModule) ? mod : { "default": mod };
32
32
  };
33
33
  Object.defineProperty(exports, "__esModule", { value: true });
34
- exports.sendToDatadog = exports.getEndpointUrl = exports.zipContents = exports.sleep = exports.convertToCSV = exports.writeFile = exports.getTags = exports.getAllLogs = exports.getLogEvents = exports.getLogStreamNames = exports.createDirectories = exports.deleteFolder = exports.maskConfig = exports.validateStartEndFlags = exports.LambdaFlareCommand = void 0;
34
+ exports.generateInsightsFile = exports.getFramework = exports.sleep = exports.convertToCSV = exports.getUniqueFileNames = exports.getTags = exports.getAllLogs = exports.getLogEvents = exports.getLogStreamNames = exports.validateFilePath = exports.getProjectFiles = exports.validateStartEndFlags = exports.LambdaFlareCommand = void 0;
35
35
  const fs = __importStar(require("fs"));
36
36
  const path = __importStar(require("path"));
37
37
  const util_1 = __importDefault(require("util"));
38
38
  const client_cloudwatch_logs_1 = require("@aws-sdk/client-cloudwatch-logs");
39
39
  const client_lambda_1 = require("@aws-sdk/client-lambda");
40
- const axios_1 = __importDefault(require("axios"));
40
+ const chalk_1 = __importDefault(require("chalk"));
41
41
  const clipanion_1 = require("clipanion");
42
- const form_data_1 = __importDefault(require("form-data"));
43
- const inquirer_1 = __importDefault(require("inquirer"));
44
- const jszip_1 = __importDefault(require("jszip"));
45
42
  const constants_1 = require("../../constants");
46
- const validation_1 = require("../../helpers/validation");
43
+ const flare_1 = require("../../helpers/flare");
44
+ const fs_1 = require("../../helpers/fs");
45
+ const prompt_1 = require("../../helpers/prompt");
46
+ const helpersRenderer = __importStar(require("../../helpers/renderer"));
47
+ const utils_1 = require("../../helpers/utils");
47
48
  const constants_2 = require("./constants");
48
49
  const commons_1 = require("./functions/commons");
49
- const prompt_1 = require("./prompt");
50
+ const prompt_2 = require("./prompt");
50
51
  const commonRenderer = __importStar(require("./renderers/common-renderer"));
51
- const flareRenderer = __importStar(require("./renderers/flare-renderer"));
52
- const { version } = require('../../../package.json');
53
- const ENDPOINT_PATH = '/api/ui/support/serverless/flare';
54
- const FLARE_OUTPUT_DIRECTORY = '.datadog-ci';
55
- const LOGS_DIRECTORY = 'logs';
52
+ const version = require('../../../package.json').version;
53
+ const PROJECT_FILES_DIRECTORY = 'project_files';
54
+ const ADDITIONAL_FILES_DIRECTORY = 'additional_files';
56
55
  const FUNCTION_CONFIG_FILE_NAME = 'function_config.json';
57
56
  const TAGS_FILE_NAME = 'tags.json';
58
- const ZIP_FILE_NAME = 'lambda-flare-output.zip';
57
+ const INSIGHTS_FILE_NAME = 'INSIGHTS.md';
58
+ const FLARE_ZIP_FILE_NAME = 'lambda-flare-output.zip';
59
59
  const MAX_LOG_STREAMS = 50;
60
60
  const DEFAULT_LOG_STREAMS = 3;
61
61
  const MAX_LOG_EVENTS_PER_STREAM = 1000;
@@ -67,16 +67,17 @@ class LambdaFlareCommand extends clipanion_1.Command {
67
67
  }
68
68
  /**
69
69
  * Entry point for the `lambda flare` command.
70
- * Gathers lambda function configuration and sends it to Datadog.
70
+ * Gathers config, logs, tags, project files, and more from a
71
+ * Lambda function and sends them to Datadog support.
71
72
  * @returns 0 if the command ran successfully, 1 otherwise.
72
73
  */
73
74
  execute() {
74
75
  var _a, _b, _c;
75
76
  return __awaiter(this, void 0, void 0, function* () {
76
- this.context.stdout.write(flareRenderer.renderLambdaFlareHeader(this.isDryRun));
77
+ this.context.stdout.write(helpersRenderer.renderFlareHeader('Lambda', this.isDryRun));
77
78
  // Validate function name
78
79
  if (this.functionName === undefined) {
79
- this.context.stderr.write(commonRenderer.renderError('No function name specified. [-f,--function]'));
80
+ this.context.stderr.write(helpersRenderer.renderError('No function name specified. [-f,--function]'));
80
81
  return 1;
81
82
  }
82
83
  const errorMessages = [];
@@ -86,18 +87,18 @@ class LambdaFlareCommand extends clipanion_1.Command {
86
87
  errorMessages.push(commonRenderer.renderNoDefaultRegionSpecifiedError());
87
88
  }
88
89
  // Validate Datadog API key
89
- this.apiKey = (_c = process.env[constants_2.CI_API_KEY_ENV_VAR]) !== null && _c !== void 0 ? _c : process.env[constants_2.API_KEY_ENV_VAR];
90
+ this.apiKey = (_c = process.env[constants_1.CI_API_KEY_ENV_VAR]) !== null && _c !== void 0 ? _c : process.env[constants_1.API_KEY_ENV_VAR];
90
91
  if (this.apiKey === undefined) {
91
- errorMessages.push(commonRenderer.renderError('No Datadog API key specified. Set an API key with the DATADOG_API_KEY environment variable.'));
92
+ errorMessages.push(helpersRenderer.renderError('No Datadog API key specified. Set an API key with the DATADOG_API_KEY environment variable.'));
92
93
  }
93
94
  if (!this.isDryRun) {
94
95
  // Validate case ID
95
96
  if (this.caseId === undefined) {
96
- errorMessages.push(commonRenderer.renderError('No case ID specified. [-c,--case-id]'));
97
+ errorMessages.push(helpersRenderer.renderError('No case ID specified. [-c,--case-id]'));
97
98
  }
98
99
  // Validate email
99
100
  if (this.email === undefined) {
100
- errorMessages.push(commonRenderer.renderError('No email specified. [-e,--email]'));
101
+ errorMessages.push(helpersRenderer.renderError('No email specified. [-e,--email]'));
101
102
  }
102
103
  }
103
104
  // Validate start/end flags if both are specified
@@ -109,7 +110,7 @@ class LambdaFlareCommand extends clipanion_1.Command {
109
110
  }
110
111
  catch (err) {
111
112
  if (err instanceof Error) {
112
- errorMessages.push(commonRenderer.renderError(err.message));
113
+ errorMessages.push(helpersRenderer.renderError(err.message));
113
114
  }
114
115
  }
115
116
  if (errorMessages.length > 0) {
@@ -119,30 +120,30 @@ class LambdaFlareCommand extends clipanion_1.Command {
119
120
  return 1;
120
121
  }
121
122
  // Get AWS credentials
122
- this.context.stdout.write('\n🔑 Getting AWS credentials...\n');
123
+ this.context.stdout.write(chalk_1.default.bold('\n🔑 Getting AWS credentials...\n'));
123
124
  try {
124
125
  this.credentials = yield commons_1.getAWSCredentials();
125
126
  }
126
127
  catch (err) {
127
128
  if (err instanceof Error) {
128
- this.context.stderr.write(commonRenderer.renderError(err.message));
129
+ this.context.stderr.write(helpersRenderer.renderError(err.message));
129
130
  }
130
131
  return 1;
131
132
  }
132
133
  if (this.credentials === undefined) {
133
134
  this.context.stdout.write('\n' + commonRenderer.renderNoAWSCredentialsFound());
134
135
  try {
135
- yield prompt_1.requestAWSCredentials();
136
+ yield prompt_2.requestAWSCredentials();
136
137
  }
137
138
  catch (err) {
138
139
  if (err instanceof Error) {
139
- this.context.stderr.write(commonRenderer.renderError(err.message));
140
+ this.context.stderr.write(helpersRenderer.renderError(err.message));
140
141
  }
141
142
  return 1;
142
143
  }
143
144
  }
144
145
  // Get and print Lambda function configuration
145
- this.context.stdout.write('\n🔍 Fetching Lambda function configuration...\n');
146
+ this.context.stdout.write(chalk_1.default.bold('\n🔍 Fetching Lambda function configuration...\n'));
146
147
  const lambdaClientConfig = {
147
148
  region,
148
149
  credentials: this.credentials,
@@ -154,42 +155,99 @@ class LambdaFlareCommand extends clipanion_1.Command {
154
155
  }
155
156
  catch (err) {
156
157
  if (err instanceof Error) {
157
- this.context.stderr.write(commonRenderer.renderError(`Unable to get Lambda function configuration: ${err.message}`));
158
+ this.context.stderr.write(helpersRenderer.renderError(`Unable to get Lambda function configuration: ${err.message}`));
158
159
  }
159
160
  return 1;
160
161
  }
161
- config = exports.maskConfig(config);
162
+ config = commons_1.maskConfig(config);
162
163
  const configStr = util_1.default.inspect(config, false, undefined, true);
163
164
  this.context.stdout.write(`\n${configStr}\n`);
165
+ // Get project files
166
+ this.context.stdout.write(chalk_1.default.bold('\n📁 Searching for project files in current directory...\n'));
167
+ const projectFilePaths = yield exports.getProjectFiles();
168
+ let projectFilesMessage = chalk_1.default.bold(`\n✅ Found project file(s) in ${process.cwd()}:\n`);
169
+ if (projectFilePaths.size === 0) {
170
+ projectFilesMessage = helpersRenderer.renderSoftWarning('No project files found.');
171
+ }
172
+ this.context.stdout.write(projectFilesMessage);
173
+ for (const filePath of projectFilePaths) {
174
+ const fileName = path.basename(filePath);
175
+ this.context.stdout.write(`• ${fileName}\n`);
176
+ }
177
+ // Additional files
178
+ this.context.stdout.write('\n');
179
+ const additionalFilePaths = new Set();
180
+ let confirmAdditionalFiles;
181
+ try {
182
+ confirmAdditionalFiles = yield prompt_1.requestConfirmation('Do you want to specify any additional files to flare?', false);
183
+ }
184
+ catch (err) {
185
+ if (err instanceof Error) {
186
+ this.context.stderr.write(helpersRenderer.renderError(err.message));
187
+ }
188
+ return 1;
189
+ }
190
+ while (confirmAdditionalFiles) {
191
+ this.context.stdout.write('\n');
192
+ let filePath;
193
+ try {
194
+ filePath = yield prompt_2.requestFilePath();
195
+ }
196
+ catch (err) {
197
+ if (err instanceof Error) {
198
+ this.context.stderr.write(helpersRenderer.renderError(err.message));
199
+ }
200
+ return 1;
201
+ }
202
+ if (filePath === '') {
203
+ this.context.stdout.write(`Added ${additionalFilePaths.size} custom file(s):\n`);
204
+ for (const additionalFilePath of additionalFilePaths) {
205
+ const fileName = path.basename(additionalFilePath);
206
+ this.context.stdout.write(`• ${fileName}\n`);
207
+ }
208
+ break;
209
+ }
210
+ try {
211
+ filePath = exports.validateFilePath(filePath, projectFilePaths, additionalFilePaths);
212
+ additionalFilePaths.add(filePath);
213
+ const fileName = path.basename(filePath);
214
+ this.context.stdout.write(`• Added file '${fileName}'\n`);
215
+ }
216
+ catch (err) {
217
+ if (err instanceof Error) {
218
+ this.context.stderr.write(err.message);
219
+ }
220
+ }
221
+ }
164
222
  // Get tags
165
- this.context.stdout.write('\n🏷 Getting Resource Tags...\n');
223
+ this.context.stdout.write(chalk_1.default.bold('\n🏷 Getting Resource Tags...\n'));
166
224
  let tags;
167
225
  try {
168
226
  tags = yield exports.getTags(lambdaClient, region, config.FunctionArn);
169
227
  }
170
228
  catch (err) {
171
229
  if (err instanceof Error) {
172
- this.context.stderr.write(commonRenderer.renderError(err.message));
230
+ this.context.stderr.write(helpersRenderer.renderError(err.message));
173
231
  }
174
232
  return 1;
175
233
  }
176
234
  const tagsLength = Object.keys(tags).length;
177
235
  if (tagsLength === 0) {
178
- this.context.stdout.write(commonRenderer.renderSoftWarning(`No resource tags were found.`));
236
+ this.context.stdout.write(helpersRenderer.renderSoftWarning(`No resource tags were found.`));
179
237
  }
180
238
  else {
181
- this.context.stdout.write(`✅ Found ${tagsLength} resource tags.\n`);
239
+ this.context.stdout.write(`Found ${tagsLength} resource tag(s).\n`);
182
240
  }
183
241
  // Get CloudWatch logs
184
242
  let logs = new Map();
185
243
  if (this.withLogs) {
186
- this.context.stdout.write('\n☁️ Getting CloudWatch logs...\n');
244
+ this.context.stdout.write(chalk_1.default.bold('\n🌧 Getting CloudWatch logs...\n'));
187
245
  try {
188
246
  logs = yield exports.getAllLogs(region, this.functionName, startMillis, endMillis);
189
247
  }
190
248
  catch (err) {
191
249
  if (err instanceof Error) {
192
- this.context.stderr.write(commonRenderer.renderError(err.message));
250
+ this.context.stderr.write(helpersRenderer.renderError(err.message));
193
251
  }
194
252
  return 1;
195
253
  }
@@ -197,48 +255,81 @@ class LambdaFlareCommand extends clipanion_1.Command {
197
255
  try {
198
256
  // CloudWatch messages
199
257
  if (this.withLogs) {
200
- let message = '\n✅ Found log streams:\n';
258
+ let message = chalk_1.default.bold('\n✅ Found log streams:\n');
201
259
  if (logs.size === 0) {
202
- message = commonRenderer.renderSoftWarning('No CloudWatch log streams were found. Logs will not be retrieved or sent.');
260
+ message = helpersRenderer.renderSoftWarning('No CloudWatch log streams were found. Logs will not be retrieved or sent.');
203
261
  }
204
262
  this.context.stdout.write(message);
205
263
  for (const [logStreamName, logEvents] of logs) {
206
264
  let warningMessage = '\n';
207
265
  if (logEvents.length === 0) {
208
- warningMessage = ' - ' + commonRenderer.renderSoftWarning('No log events found in this stream');
266
+ warningMessage = ` - ${helpersRenderer.renderSoftWarning('No log events found in this stream')}`;
209
267
  }
210
268
  this.context.stdout.write(`• ${logStreamName}${warningMessage}`);
211
269
  }
212
270
  }
213
271
  // Create folders
214
- this.context.stdout.write('\n💾 Saving files...\n');
215
- const rootFolderPath = path.join(process.cwd(), FLARE_OUTPUT_DIRECTORY);
216
- const logsFolderPath = path.join(rootFolderPath, LOGS_DIRECTORY);
272
+ const rootFolderPath = path.join(process.cwd(), constants_1.FLARE_OUTPUT_DIRECTORY);
273
+ const logsFolderPath = path.join(rootFolderPath, constants_1.LOGS_DIRECTORY);
274
+ const projectFilesFolderPath = path.join(rootFolderPath, PROJECT_FILES_DIRECTORY);
275
+ const additionalFilesFolderPath = path.join(rootFolderPath, ADDITIONAL_FILES_DIRECTORY);
276
+ this.context.stdout.write(chalk_1.default.bold(`\n💾 Saving files to ${rootFolderPath}...\n`));
217
277
  if (fs.existsSync(rootFolderPath)) {
218
- exports.deleteFolder(rootFolderPath);
278
+ fs_1.deleteFolder(rootFolderPath);
279
+ }
280
+ const subFolders = [];
281
+ if (logs.size > 0) {
282
+ subFolders.push(logsFolderPath);
219
283
  }
220
- exports.createDirectories(rootFolderPath, logsFolderPath, logs);
221
- // Write files
284
+ if (projectFilePaths.size > 0) {
285
+ subFolders.push(projectFilesFolderPath);
286
+ }
287
+ if (additionalFilePaths.size > 0) {
288
+ subFolders.push(additionalFilesFolderPath);
289
+ }
290
+ fs_1.createDirectories(rootFolderPath, subFolders);
291
+ // Write config file
222
292
  const configFilePath = path.join(rootFolderPath, FUNCTION_CONFIG_FILE_NAME);
223
- exports.writeFile(configFilePath, JSON.stringify(config, undefined, 2));
224
- this.context.stdout.write(`• Saved function config to ${configFilePath}\n`);
293
+ fs_1.writeFile(configFilePath, JSON.stringify(config, undefined, 2));
294
+ this.context.stdout.write(`• Saved function config to ./${FUNCTION_CONFIG_FILE_NAME}\n`);
295
+ // Write tags file
225
296
  if (tagsLength > 0) {
226
297
  const tagsFilePath = path.join(rootFolderPath, TAGS_FILE_NAME);
227
- exports.writeFile(tagsFilePath, JSON.stringify(tags, undefined, 2));
228
- this.context.stdout.write(`• Saved tags to ${tagsFilePath}\n`);
298
+ fs_1.writeFile(tagsFilePath, JSON.stringify(tags, undefined, 2));
299
+ this.context.stdout.write(`• Saved tags to ./${TAGS_FILE_NAME}\n`);
229
300
  }
301
+ // Write log files
230
302
  for (const [logStreamName, logEvents] of logs) {
231
303
  if (logEvents.length === 0) {
232
304
  continue;
233
305
  }
234
306
  const logFilePath = path.join(logsFolderPath, `${logStreamName.split('/').join('-')}.csv`);
235
307
  const data = exports.convertToCSV(logEvents);
236
- exports.writeFile(logFilePath, data);
237
- this.context.stdout.write(`• Saved logs to ${logFilePath}\n`);
308
+ fs_1.writeFile(logFilePath, data);
309
+ this.context.stdout.write(`• Saved logs to ./${constants_1.LOGS_DIRECTORY}/${logStreamName}\n`);
238
310
  // Sleep for 1 millisecond so creation times are different
239
311
  // This allows the logs to be sorted by creation time by the support team
240
312
  yield exports.sleep(1);
241
313
  }
314
+ // Write project files
315
+ for (const filePath of projectFilePaths) {
316
+ const fileName = path.basename(filePath);
317
+ const newFilePath = path.join(projectFilesFolderPath, fileName);
318
+ fs.copyFileSync(filePath, newFilePath);
319
+ this.context.stdout.write(`• Copied ${fileName} to ./${PROJECT_FILES_DIRECTORY}/${fileName}\n`);
320
+ }
321
+ // Write additional files
322
+ const additionalFilesMap = exports.getUniqueFileNames(additionalFilePaths);
323
+ for (const [originalFilePath, newFileName] of additionalFilesMap) {
324
+ const originalFileName = path.basename(originalFilePath);
325
+ const newFilePath = path.join(additionalFilesFolderPath, newFileName);
326
+ fs.copyFileSync(originalFilePath, newFilePath);
327
+ this.context.stdout.write(`• Copied ${originalFileName} to ./${ADDITIONAL_FILES_DIRECTORY}/${newFileName}\n`);
328
+ }
329
+ // Write insights file
330
+ const insightsFilePath = path.join(rootFolderPath, INSIGHTS_FILE_NAME);
331
+ exports.generateInsightsFile(insightsFilePath, this.isDryRun, config);
332
+ this.context.stdout.write(`• Saved insights file to ./${INSIGHTS_FILE_NAME}\n`);
242
333
  // Exit if dry run
243
334
  const outputMsg = `\nℹ️ Your output files are located at: ${rootFolderPath}\n\n`;
244
335
  if (this.isDryRun) {
@@ -248,25 +339,25 @@ class LambdaFlareCommand extends clipanion_1.Command {
248
339
  }
249
340
  // Confirm before sending
250
341
  this.context.stdout.write('\n');
251
- const answer = yield inquirer_1.default.prompt(prompt_1.confirmationQuestion('Are you sure you want to send the flare file to Datadog Support?'));
252
- if (!answer.confirmation) {
342
+ const confirmSendFiles = yield prompt_1.requestConfirmation('Are you sure you want to send the flare file to Datadog Support?', false);
343
+ if (!confirmSendFiles) {
253
344
  this.context.stdout.write('\n🚫 The flare files were not sent based on your selection.');
254
345
  this.context.stdout.write(outputMsg);
255
346
  return 0;
256
347
  }
257
348
  // Zip folder
258
- const zipPath = path.join(rootFolderPath, ZIP_FILE_NAME);
259
- yield exports.zipContents(rootFolderPath, zipPath);
349
+ const zipPath = path.join(rootFolderPath, FLARE_ZIP_FILE_NAME);
350
+ yield fs_1.zipContents(rootFolderPath, zipPath);
260
351
  // Send to Datadog
261
- this.context.stdout.write('\n🚀 Sending to Datadog Support...\n');
262
- yield exports.sendToDatadog(zipPath, this.caseId, this.email, this.apiKey, rootFolderPath);
263
- this.context.stdout.write('\n✅ Successfully sent flare file to Datadog Support!\n');
352
+ this.context.stdout.write(chalk_1.default.bold('\n🚀 Sending to Datadog Support...\n'));
353
+ yield flare_1.sendToDatadog(zipPath, this.caseId, this.email, this.apiKey, rootFolderPath);
354
+ this.context.stdout.write(chalk_1.default.bold('\n✅ Successfully sent flare file to Datadog Support!\n'));
264
355
  // Delete contents
265
- exports.deleteFolder(rootFolderPath);
356
+ fs_1.deleteFolder(rootFolderPath);
266
357
  }
267
358
  catch (err) {
268
359
  if (err instanceof Error) {
269
- this.context.stderr.write(commonRenderer.renderError(err.message));
360
+ this.context.stderr.write(helpersRenderer.renderError(err.message));
270
361
  }
271
362
  return 1;
272
363
  }
@@ -309,57 +400,42 @@ const validateStartEndFlags = (start, end) => {
309
400
  };
310
401
  exports.validateStartEndFlags = validateStartEndFlags;
311
402
  /**
312
- * Mask the environment variables in a Lambda function configuration
313
- * @param config
403
+ * Searches current directory for project files
404
+ * @returns a set of file paths of project files
314
405
  */
315
- const maskConfig = (config) => {
316
- var _a;
317
- const environmentVariables = (_a = config.Environment) === null || _a === void 0 ? void 0 : _a.Variables;
318
- if (!environmentVariables) {
319
- return config;
320
- }
321
- const replacer = commons_1.maskStringifiedEnvVar(environmentVariables);
322
- const stringifiedConfig = JSON.stringify(config, replacer);
323
- return JSON.parse(stringifiedConfig);
324
- };
325
- exports.maskConfig = maskConfig;
326
- /**
327
- * Delete a folder and all its contents
328
- * @param folderPath the folder to delete
329
- * @throws Error if the deletion fails
330
- */
331
- const deleteFolder = (folderPath) => {
332
- try {
333
- fs.rmSync(folderPath, { recursive: true, force: true });
334
- }
335
- catch (err) {
336
- if (err instanceof Error) {
337
- throw Error(`Failed to delete files located at ${folderPath}: ${err.message}`);
406
+ const getProjectFiles = () => __awaiter(void 0, void 0, void 0, function* () {
407
+ const filePaths = new Set();
408
+ const cwd = process.cwd();
409
+ for (const fileName of constants_2.PROJECT_FILES) {
410
+ const filePath = path.join(cwd, fileName);
411
+ if (fs.existsSync(filePath)) {
412
+ filePaths.add(filePath);
338
413
  }
339
414
  }
340
- };
341
- exports.deleteFolder = deleteFolder;
415
+ return filePaths;
416
+ });
417
+ exports.getProjectFiles = getProjectFiles;
342
418
  /**
343
- * Creates the root folder and the logs sub-folder
344
- * @param rootFolderPath path to the root folder
345
- * @param logsFolderPath path to the logs folder
346
- * @param logs array of logs
347
- * @throws Error if the root folder cannot be deleted or folders cannot be created
419
+ * Validates a path to a file
420
+ * @param filePath path to the file
421
+ * @param projectFilePaths map of file names to file paths
422
+ * @param additionalFiles set of additional file paths
423
+ * @throws Error if the file path is invalid or the file was already added
424
+ * @returns the full path to the file
348
425
  */
349
- const createDirectories = (rootFolderPath, logsFolderPath, logs) => {
350
- try {
351
- fs.mkdirSync(rootFolderPath);
352
- if (logs.size > 0) {
353
- fs.mkdirSync(logsFolderPath);
354
- }
426
+ const validateFilePath = (filePath, projectFilePaths, additionalFiles) => {
427
+ const originalPath = filePath;
428
+ filePath = fs.existsSync(filePath) ? filePath : path.join(process.cwd(), filePath);
429
+ if (!fs.existsSync(filePath)) {
430
+ throw Error(helpersRenderer.renderError(`File path '${originalPath}' not found. Please try again.`));
355
431
  }
356
- catch (err) {
357
- if (err instanceof Error) {
358
- throw Error(`Unable to create directories: ${err.message}`);
359
- }
432
+ filePath = path.resolve(filePath);
433
+ if (projectFilePaths.has(filePath) || additionalFiles.has(filePath)) {
434
+ throw Error(helpersRenderer.renderSoftWarning(`File '${filePath}' has already been added.`));
360
435
  }
436
+ return filePath;
361
437
  };
362
- exports.createDirectories = createDirectories;
438
+ exports.validateFilePath = validateFilePath;
363
439
  /**
364
440
  * Gets the LOG_STREAM_COUNT latest log stream names, sorted by last event time
365
441
  * @param cwlClient CloudWatch Logs client
@@ -504,22 +580,39 @@ const getTags = (lambdaClient, region, arn) => __awaiter(void 0, void 0, void 0,
504
580
  });
505
581
  exports.getTags = getTags;
506
582
  /**
507
- * Write the function config to a file
508
- * @param filePath path to the file
509
- * @param data the data to write
510
- * @throws Error if the file cannot be written
583
+ * Generate unique file names
584
+ * If the original file name is unique, keep it as is
585
+ * Otherwise, replace separators in the file path with dashes
586
+ * @param filePaths the list of file paths
587
+ * @returns a mapping of file paths to new file names
511
588
  */
512
- const writeFile = (filePath, data) => {
513
- try {
514
- fs.writeFileSync(filePath, data);
515
- }
516
- catch (err) {
517
- if (err instanceof Error) {
518
- throw Error(`Unable to create function configuration file: ${err.message}`);
589
+ const getUniqueFileNames = (filePaths) => {
590
+ // Count occurrences of each filename
591
+ const fileNameCount = {};
592
+ filePaths.forEach((filePath) => {
593
+ const fileName = path.basename(filePath);
594
+ const count = fileNameCount[fileName] || 0;
595
+ fileNameCount[fileName] = count + 1;
596
+ });
597
+ // Create new filenames
598
+ const filePathsToNewFileNames = new Map();
599
+ filePaths.forEach((filePath) => {
600
+ const fileName = path.basename(filePath);
601
+ if (fileNameCount[fileName] > 1) {
602
+ // Trim leading and trailing '/'s and '\'s
603
+ const trimRegex = /^\/+|\/+$/g;
604
+ const filePathTrimmed = filePath.replace(trimRegex, '');
605
+ // Replace '/'s and '\'s with '-'s
606
+ const newFileName = filePathTrimmed.split(path.sep).join('-');
607
+ filePathsToNewFileNames.set(filePath, newFileName);
519
608
  }
520
- }
609
+ else {
610
+ filePathsToNewFileNames.set(filePath, fileName);
611
+ }
612
+ });
613
+ return filePathsToNewFileNames;
521
614
  };
522
- exports.writeFile = writeFile;
615
+ exports.getUniqueFileNames = getUniqueFileNames;
523
616
  /**
524
617
  * Convert the log events to a CSV string
525
618
  * @param logEvents array of log events
@@ -549,101 +642,81 @@ const sleep = (ms) => __awaiter(void 0, void 0, void 0, function* () {
549
642
  });
550
643
  exports.sleep = sleep;
551
644
  /**
552
- * Zip the contents of the flare folder
553
- * @param rootFolderPath path to the root folder to zip
554
- * @param zipPath path to save the zip file
555
- * @throws Error if the zip fails
645
+ * Get the framework used based on the files in the directory
646
+ * @returns the framework used or undefined if no framework is found
556
647
  */
557
- const zipContents = (rootFolderPath, zipPath) => __awaiter(void 0, void 0, void 0, function* () {
558
- const zip = new jszip_1.default();
559
- const addFolderToZip = (folderPath) => {
560
- if (!fs.existsSync(folderPath)) {
561
- throw Error(`Folder does not exist: ${folderPath}`);
562
- }
563
- const folder = fs.statSync(folderPath);
564
- if (!folder.isDirectory()) {
565
- throw Error(`Path is not a directory: ${folderPath}`);
648
+ const getFramework = () => {
649
+ const frameworks = new Set();
650
+ const files = fs.readdirSync(process.cwd());
651
+ files.forEach((file) => {
652
+ if (constants_2.FRAMEWORK_FILES_MAPPING.has(file)) {
653
+ frameworks.add(constants_2.FRAMEWORK_FILES_MAPPING.get(file));
566
654
  }
567
- const contents = fs.readdirSync(folderPath);
568
- for (const item of contents) {
569
- const fullPath = path.join(folderPath, item);
570
- const file = fs.statSync(fullPath);
571
- if (file.isDirectory()) {
572
- addFolderToZip(fullPath);
573
- }
574
- else {
575
- const data = fs.readFileSync(fullPath);
576
- zip.file(path.relative(rootFolderPath, fullPath), data);
577
- }
578
- }
579
- };
580
- try {
581
- addFolderToZip(rootFolderPath);
582
- const zipContent = yield zip.generateAsync({ type: 'nodebuffer' });
583
- fs.writeFileSync(zipPath, zipContent);
584
- }
585
- catch (err) {
586
- if (err instanceof Error) {
587
- throw Error(`Unable to zip the flare files: ${err.message}`);
588
- }
589
- }
590
- });
591
- exports.zipContents = zipContents;
592
- /**
593
- * Calculates the full endpoint URL
594
- * @throws Error if the site is invalid
595
- * @returns the full endpoint URL
596
- */
597
- const getEndpointUrl = () => {
598
- var _a, _b;
599
- const baseUrl = (_b = (_a = process.env[constants_2.CI_SITE_ENV_VAR]) !== null && _a !== void 0 ? _a : process.env[constants_2.SITE_ENV_VAR]) !== null && _b !== void 0 ? _b : constants_1.DATADOG_SITE_US1;
600
- // The DNS doesn't redirect to the proper endpoint when a subdomain is not present in the baseUrl.
601
- // There is a DNS inconsistency
602
- let endpointUrl = baseUrl;
603
- if ([constants_1.DATADOG_SITE_US1, constants_1.DATADOG_SITE_EU1, constants_1.DATADOG_SITE_GOV].includes(baseUrl)) {
604
- endpointUrl = 'app.' + baseUrl;
605
- }
606
- if (!validation_1.isValidDatadogSite(baseUrl)) {
607
- throw Error(`Invalid site: ${baseUrl}. Must be one of: ${constants_1.DATADOG_SITES.join(', ')}`);
655
+ });
656
+ if (frameworks.size > 0) {
657
+ return Array.from(frameworks).join(', ');
608
658
  }
609
- return 'https://' + endpointUrl + ENDPOINT_PATH;
659
+ return constants_2.DeploymentFrameworks.Unknown;
610
660
  };
611
- exports.getEndpointUrl = getEndpointUrl;
661
+ exports.getFramework = getFramework;
612
662
  /**
613
- * Send the zip file to Datadog support
614
- * @param zipPath
615
- * @param caseId
616
- * @param email
617
- * @param apiKey
618
- * @param rootFolderPath
619
- * @throws Error if the request fails
663
+ * Generate the insights file
664
+ * @param insightsFilePath path to the insights file
665
+ * @param isDryRun whether or not this is a dry run
666
+ * @param config Lambda function configuration
620
667
  */
621
- const sendToDatadog = (zipPath, caseId, email, apiKey, rootFolderPath) => __awaiter(void 0, void 0, void 0, function* () {
622
- var _b, _c, _d;
623
- const endpointUrl = exports.getEndpointUrl();
624
- const form = new form_data_1.default();
625
- form.append('case_id', caseId);
626
- form.append('flare_file', fs.createReadStream(zipPath));
627
- form.append('datadog_ci_version', version);
628
- form.append('email', email);
629
- const headerConfig = {
630
- headers: Object.assign(Object.assign({}, form.getHeaders()), { 'DD-API-KEY': apiKey }),
631
- };
632
- try {
633
- yield axios_1.default.post(endpointUrl, form, headerConfig);
634
- }
635
- catch (err) {
636
- // Ensure the root folder is deleted if the request fails
637
- exports.deleteFolder(rootFolderPath);
638
- if (axios_1.default.isAxiosError(err)) {
639
- const errResponse = (_c = (_b = err.response) === null || _b === void 0 ? void 0 : _b.data.error) !== null && _c !== void 0 ? _c : '';
640
- const errorMessage = (_d = err.message) !== null && _d !== void 0 ? _d : '';
641
- throw Error(`Failed to send flare file to Datadog Support: ${errorMessage}. ${errResponse}\n`);
668
+ const generateInsightsFile = (insightsFilePath, isDryRun, config) => {
669
+ var _a, _b, _c, _d, _e;
670
+ const lines = [];
671
+ // Header
672
+ lines.push('# Flare Insights');
673
+ lines.push('\n_Autogenerated file from `lambda flare`_ ');
674
+ if (isDryRun) {
675
+ lines.push('_This command was run in dry mode._');
676
+ }
677
+ // AWS Lambda Configuration
678
+ lines.push('\n## AWS Lambda Configuration');
679
+ lines.push(`**Function Name**: \`${config.FunctionName}\` `);
680
+ lines.push(`**Function ARN**: \`${config.FunctionArn}\` `);
681
+ lines.push(`**Runtime**: \`${config.Runtime}\` `);
682
+ lines.push(`**Handler**: \`${config.Handler}\` `);
683
+ lines.push(`**Timeout**: \`${config.Timeout}\` `);
684
+ lines.push(`**Memory Size**: \`${config.MemorySize}\` `);
685
+ const architectures = (_a = config.Architectures) !== null && _a !== void 0 ? _a : ['Unknown'];
686
+ lines.push(`**Architecture**: \`${architectures.join(', ')}\` `);
687
+ lines.push('**Environment Variables**:');
688
+ const envVars = Object.entries((_c = (_b = config.Environment) === null || _b === void 0 ? void 0 : _b.Variables) !== null && _c !== void 0 ? _c : {});
689
+ if (envVars.length === 0) {
690
+ lines.push('- No environment variables found.');
691
+ }
692
+ for (const [key, value] of envVars) {
693
+ lines.push(`- \`${key}\`: \`${value}\``);
694
+ }
695
+ lines.push('\n**Layers**:');
696
+ const layers = (_d = config.Layers) !== null && _d !== void 0 ? _d : [];
697
+ if (layers.length === 0) {
698
+ lines.push(' - No layers found.');
699
+ }
700
+ let codeSize = (_e = config.CodeSize) !== null && _e !== void 0 ? _e : 0;
701
+ layers.forEach((layer) => {
702
+ var _a, _b;
703
+ const nameAndVersion = commons_1.getLayerNameWithVersion((_a = layer.Arn) !== null && _a !== void 0 ? _a : '');
704
+ if (nameAndVersion) {
705
+ lines.push(`- \`${nameAndVersion}\``);
642
706
  }
643
- throw err;
644
- }
645
- });
646
- exports.sendToDatadog = sendToDatadog;
707
+ codeSize += (_b = layer.CodeSize) !== null && _b !== void 0 ? _b : 0;
708
+ });
709
+ lines.push(`\n**Package Size**: \`${utils_1.formatBytes(codeSize)}\``);
710
+ // CLI Insights
711
+ lines.push('\n ## CLI');
712
+ lines.push(`**Run Location**: \`${process.cwd()}\` `);
713
+ lines.push(`**CLI Version**: \`${version}\` `);
714
+ const timeString = new Date().toISOString().replace('T', ' ').replace('Z', '') + ' UTC';
715
+ lines.push(`**Timestamp**: \`${timeString}\` `);
716
+ lines.push(`**Framework**: \`${exports.getFramework()}\``);
717
+ fs_1.writeFile(insightsFilePath, lines.join('\n'));
718
+ };
719
+ exports.generateInsightsFile = generateInsightsFile;
647
720
  LambdaFlareCommand.addPath('lambda', 'flare');
648
721
  LambdaFlareCommand.addOption('isDryRun', clipanion_1.Command.Boolean('-d,--dry'));
649
722
  LambdaFlareCommand.addOption('withLogs', clipanion_1.Command.Boolean('--with-logs'));