Skip to content

Commit efd0b4e

Browse files
committed
feat: analytics
1 parent e711405 commit efd0b4e

File tree

11 files changed

+311
-13
lines changed

11 files changed

+311
-13
lines changed

.envrc.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22

33
export GRAPHQL_HOST='https://api.nes.herodevs.com';
44
export EOL_REPORT_URL='https://eol-report-card.apps.herodevs.com/reports';
5+
export ANALYTICS_URL='https://eol-api.herodevs.com/track';

package-lock.json

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@
3636
"herodevs cli"
3737
],
3838
"dependencies": {
39+
"@amplitude/analytics-node": "^1.5.0",
3940
"@apollo/client": "^3.13.8",
4041
"@cyclonedx/cdxgen": "^11.4.3",
4142
"@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.4",
4243
"@oclif/core": "^4.4.0",
4344
"@oclif/plugin-help": "^6.2.29",
4445
"@oclif/plugin-update": "^4.6.45",
4546
"graphql": "^16.11.0",
47+
"node-machine-id": "^1.1.12",
4648
"ora": "^8.2.0",
4749
"packageurl-js": "^2.0.1",
4850
"terminal-link": "^4.0.0",
@@ -84,7 +86,8 @@
8486
],
8587
"hooks": {
8688
"init": "./dist/hooks/npm-update-notifier.js",
87-
"prerun": "./dist/hooks/prerun.js"
89+
"prerun": "./dist/hooks/prerun.js",
90+
"finally": "./dist/hooks/finally.js"
8891
},
8992
"topicSeparator": " ",
9093
"macos": {

src/commands/scan/eol.ts

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { Command, Flags } from '@oclif/core';
44
import ora from 'ora';
55
import { submitScan } from '../../api/nes.client.ts';
66
import { config, filenamePrefix } from '../../config/constants.ts';
7+
import { track } from '../../service/analytics.svc.ts';
78
import { createSbom } from '../../service/cdx.svc.ts';
8-
import { formatScanResults, formatWebReportUrl } from '../../service/display.svc.ts';
9+
import { countComponentsByStatus, formatScanResults, formatWebReportUrl } from '../../service/display.svc.ts';
910
import { readSbomFromFile, saveReportToFile, saveSbomToFile, validateDirectory } from '../../service/file.svc.ts';
1011
import { getErrorMessage } from '../../service/log.svc.ts';
1112

@@ -56,23 +57,81 @@ export default class ScanEol extends Command {
5657
public async run(): Promise<EolReport | undefined> {
5758
const { flags } = await this.parse(ScanEol);
5859

60+
if (!flags.file) {
61+
track('CLI SBOM Generation Started', (context) => ({
62+
command: context.command,
63+
command_flags: context.command_flags,
64+
scan_location: flags.dir,
65+
}));
66+
}
67+
68+
const sbomStartTime = performance.now();
5969
const sbom = await this.loadSbom();
70+
const sbomEndTime = performance.now();
71+
72+
if (!flags.file) {
73+
track('CLI SBOM Generation Completed', (context) => ({
74+
command: context.command,
75+
command_flags: context.command_flags,
76+
scan_location: flags.dir,
77+
sbom_created: true,
78+
sbom_load_time: (sbomEndTime - sbomStartTime) / 1000,
79+
}));
80+
}
6081

6182
if (!sbom.components?.length) {
83+
track('CLI No Components Found, Report Not Generated', (context) => ({
84+
command: context.command,
85+
command_flags: context.command_flags,
86+
scan_location: flags.dir,
87+
}));
6288
this.log('No components found in scan. Report not generated.');
6389
return;
6490
}
6591

92+
track('CLI EOL Scan Started', (context) => ({
93+
command: context.command,
94+
command_flags: context.command_flags,
95+
scan_location: flags.dir,
96+
}));
97+
98+
const scanStartTime = performance.now();
6699
const scan = await this.scanSbom(sbom);
100+
const scanEndTime = performance.now();
101+
102+
const componentCounts = countComponentsByStatus(scan);
103+
track('CLI EOL Scan Completed', (context) => ({
104+
command: context.command,
105+
command_flags: context.command_flags,
106+
eol_true_count: componentCounts.EOL,
107+
eol_unknown_count: componentCounts.UNKNOWN,
108+
nes_available_count: componentCounts.NES_AVAILABLE,
109+
number_of_packages: componentCounts.TOTAL,
110+
sbom_created: !flags.file,
111+
scan_location: flags.dir,
112+
scan_load_time: (scanEndTime - scanStartTime) / 1000,
113+
scanned_ecosystems: componentCounts.ECOSYSTEMS,
114+
web_report_link: scan.id ? `${config.eolReportUrl}/${scan.id}` : undefined,
115+
}));
67116

68117
if (flags.save) {
69118
const reportPath = this.saveReport(scan, flags.dir);
70119
this.log(`Report saved to ${reportPath}`);
120+
track('CLI JSON Scan Output Saved', (context) => ({
121+
command: context.command,
122+
command_flags: context.command_flags,
123+
report_output_path: reportPath,
124+
}));
71125
}
72126

73127
if (flags.saveSbom && !flags.file) {
74128
const sbomPath = this.saveSbom(flags.dir, sbom);
75129
this.log(`SBOM saved to ${sbomPath}`);
130+
track('CLI SBOM Output Saved', (context) => ({
131+
command: context.command,
132+
command_flags: context.command_flags,
133+
sbom_output_path: sbomPath,
134+
}));
76135
}
77136

78137
if (!this.jsonEnabled()) {
@@ -107,23 +166,34 @@ export default class ScanEol extends Command {
107166
return scan;
108167
} catch (error) {
109168
spinner.fail('Scanning failed');
110-
this.error(`Failed to submit scan to NES. ${getErrorMessage(error)}`);
169+
const errorMessage = getErrorMessage(error);
170+
track('CLI EOL Scan Failed', (context) => ({
171+
command: context.command,
172+
command_flags: context.command_flags,
173+
scan_location: context.scan_location,
174+
scan_failure_reason: errorMessage,
175+
}));
176+
this.error(`Failed to submit scan to NES. ${errorMessage}`);
111177
}
112178
}
113179

114180
private saveReport(report: EolReport, dir: string): string {
115181
try {
116182
return saveReportToFile(dir, report);
117183
} catch (error) {
118-
this.error(getErrorMessage(error));
184+
const errorMessage = getErrorMessage(error);
185+
track('CLI Error Encountered', () => ({ error: errorMessage }));
186+
this.error(errorMessage);
119187
}
120188
}
121189

122190
private saveSbom(dir: string, sbom: CdxBom): string {
123191
try {
124192
return saveSbomToFile(dir, sbom);
125193
} catch (error) {
126-
this.error(getErrorMessage(error));
194+
const errorMessage = getErrorMessage(error);
195+
track('CLI Error Encountered', () => ({ error: errorMessage }));
196+
this.error(errorMessage);
127197
}
128198
}
129199

@@ -154,15 +224,19 @@ export default class ScanEol extends Command {
154224
}
155225
return sbom;
156226
} catch (error) {
157-
this.error(`Failed to scan directory: ${getErrorMessage(error)}`);
227+
const errorMessage = getErrorMessage(error);
228+
track('CLI Error Encountered', () => ({ error: errorMessage }));
229+
this.error(`Failed to scan directory: ${errorMessage}`);
158230
}
159231
}
160232

161233
private getSbomFromFile(filePath: string): CdxBom {
162234
try {
163235
return readSbomFromFile(filePath);
164236
} catch (error) {
165-
this.error(getErrorMessage(error));
237+
const errorMessage = getErrorMessage(error);
238+
track('CLI Error Encountered', () => ({ error: errorMessage }));
239+
this.error(errorMessage);
166240
}
167241
}
168242
}

src/config/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
export const EOL_REPORT_URL = 'https://eol-report-card.apps.herodevs.com/reports';
22
export const GRAPHQL_HOST = 'https://api.nes.herodevs.com';
33
export const GRAPHQL_PATH = '/graphql';
4+
export const ANALYTICS_URL = 'https://eol-api.herodevs.com/track';
45

56
export const config = {
67
eolReportUrl: process.env.EOL_REPORT_URL || EOL_REPORT_URL,
78
graphqlHost: process.env.GRAPHQL_HOST || GRAPHQL_HOST,
89
graphqlPath: process.env.GRAPHQL_PATH || GRAPHQL_PATH,
10+
analyticsUrl: process.env.ANALYTICS_URL || ANALYTICS_URL,
911
showVulnCount: true,
1012
};
1113

src/hooks/finally.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { Hook } from '@oclif/core';
2+
import ora from 'ora';
3+
import { track } from '../service/analytics.svc.ts';
4+
5+
const hook: Hook<'finally'> = async () => {
6+
const spinner = ora().start('Cleaning up');
7+
track('CLI Session Ended', (context) => ({
8+
cli_version: context.cli_version,
9+
ended_at: new Date(),
10+
})).promise.then(() => spinner.stop());
11+
};
12+
13+
export default hook;

src/hooks/prerun.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import type { Hook } from '@oclif/core';
22
import debug from 'debug';
3+
import { initializeAnalytics, track } from '../service/analytics.svc.ts';
34

45
const hook: Hook<'prerun'> = async (opts) => {
6+
initializeAnalytics();
7+
track('CLI Session Started', (context) => ({
8+
app_used: context.app_used,
9+
ci_provider: context.ci_provider,
10+
cli_version: context.cli_version,
11+
started_at: context.started_at,
12+
}));
13+
514
// If JSON flag is enabled, silence debug logging
615
if (opts.Command.prototype.jsonEnabled()) {
716
debug.disable();

0 commit comments

Comments
 (0)