Skip to content

Commit f7f2961

Browse files
author
Craig Furman
committedJul 22, 2021
feat: track IaC local execution tests [CC-972]
Call new endpoint introduced in https://github.com/snyk/registry/pull/22111 to track billing usage for IaC tests, which by default run in "local exec" mode.
1 parent 9e7f79a commit f7f2961

File tree

6 files changed

+178
-0
lines changed

6 files changed

+178
-0
lines changed
 

‎src/cli/commands/test/iac-local-execution/analytics.ts

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export enum PerformanceAnalyticsKey {
3535
OrgSettings = 'org-settings-ms',
3636
CustomSeverities = 'custom-severities-ms',
3737
ResultFormatting = 'results-formatting-ms',
38+
UsageTracking = 'usage-tracking-ms',
3839
CacheCleanup = 'cache-cleanup-ms',
3940
Total = 'total-iac-ms',
4041
}
@@ -50,6 +51,7 @@ export const performanceAnalyticsObject: Record<
5051
[PerformanceAnalyticsKey.OrgSettings]: null,
5152
[PerformanceAnalyticsKey.CustomSeverities]: null,
5253
[PerformanceAnalyticsKey.ResultFormatting]: null,
54+
[PerformanceAnalyticsKey.UsageTracking]: null,
5355
[PerformanceAnalyticsKey.CacheCleanup]: null,
5456
[PerformanceAnalyticsKey.Total]: null,
5557
};

‎src/cli/commands/test/iac-local-execution/index.ts

+13
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
EngineType,
99
} from './types';
1010
import { addIacAnalytics } from './analytics';
11+
import { TestLimitReachedError } from './usage-tracking';
1112
import { TestResult } from '../../../../lib/snyk-test/legacy';
1213
import {
1314
initLocalCache,
@@ -17,6 +18,7 @@ import {
1718
getIacOrgSettings,
1819
applyCustomSeverities,
1920
formatScanResults,
21+
trackUsage,
2022
cleanLocalCache,
2123
} from './measurable-methods';
2224
import { isFeatureFlagSupportedForOrg } from '../../../../lib/feature-flags';
@@ -62,6 +64,17 @@ export async function test(
6264
options,
6365
iacOrgSettings.meta,
6466
);
67+
68+
try {
69+
await trackUsage(formattedResults);
70+
} catch (e) {
71+
if (e instanceof TestLimitReachedError) {
72+
throw e;
73+
}
74+
// If something has gone wrong, err on the side of allowing the user to
75+
// run their tests by squashing the error.
76+
}
77+
6578
addIacAnalytics(formattedResults);
6679

6780
// TODO: add support for proper typing of old TestResult interface.

‎src/cli/commands/test/iac-local-execution/measurable-methods.ts

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { loadFiles } from './file-loader';
22
import { parseFiles } from './file-parser';
33
import { scanFiles } from './file-scanner';
44
import { formatScanResults } from './results-formatter';
5+
import { trackUsage } from './usage-tracking';
56
import { cleanLocalCache, initLocalCache } from './local-cache';
67
import { applyCustomSeverities } from './org-settings/apply-custom-severities';
78
import { getIacOrgSettings } from './org-settings/get-iac-org-settings';
@@ -87,6 +88,11 @@ const measurableFormatScanResults = performanceAnalyticsDecorator(
8788
PerformanceAnalyticsKey.ResultFormatting,
8889
);
8990

91+
const measurableTrackUsage = asyncPerformanceAnalyticsDecorator(
92+
trackUsage,
93+
PerformanceAnalyticsKey.UsageTracking,
94+
);
95+
9096
const measurableLocalTest = asyncPerformanceAnalyticsDecorator(
9197
test,
9298
PerformanceAnalyticsKey.Total,
@@ -100,6 +106,7 @@ export {
100106
measurableGetIacOrgSettings as getIacOrgSettings,
101107
measurableApplyCustomSeverities as applyCustomSeverities,
102108
measurableFormatScanResults as formatScanResults,
109+
measurableTrackUsage as trackUsage,
103110
measurableCleanLocalCache as cleanLocalCache,
104111
measurableLocalTest as localTest,
105112
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import request = require('../../../../lib/request');
2+
import config = require('../../../../lib/config');
3+
import { api as getApiToken } from '../../../../lib/api-token';
4+
import { CustomError } from '../../../../lib/errors';
5+
6+
export async function trackUsage(
7+
formattedResults: TrackableResult[],
8+
): Promise<void> {
9+
const trackingData = formattedResults.map((res) => {
10+
return {
11+
isPrivate: res.meta.isPrivate,
12+
issuesPrevented: res.result.cloudConfigResults.length,
13+
};
14+
});
15+
const trackingResponse = await request({
16+
method: 'POST',
17+
headers: {
18+
Authorization: `token ${getApiToken()}`,
19+
},
20+
url: `${config.API}/track-iac-usage/cli`,
21+
body: { results: trackingData },
22+
gzip: true,
23+
json: true,
24+
});
25+
switch (trackingResponse.res.statusCode) {
26+
case 200:
27+
break;
28+
case 429:
29+
throw new TestLimitReachedError();
30+
default:
31+
throw new CustomError(
32+
'An error occurred while attempting to track test usage: ' +
33+
JSON.stringify(trackingResponse.res.body),
34+
);
35+
}
36+
}
37+
38+
export class TestLimitReachedError extends CustomError {
39+
constructor() {
40+
super(
41+
'Test limit reached! You have exceeded your infrastructure as code test allocation for this billing period.',
42+
);
43+
}
44+
}
45+
46+
// Sub-interface of FormattedResult that we really only use to make test
47+
// fixtures easier to create.
48+
export interface TrackableResult {
49+
meta: {
50+
isPrivate: boolean;
51+
};
52+
result: {
53+
cloudConfigResults: any[];
54+
};
55+
}

‎test/acceptance/fake-server.ts

+6
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,12 @@ export function fakeServer(root, apikey) {
338338
return next();
339339
});
340340

341+
server.post(root + '/track-iac-usage/cli', (req, res, next) => {
342+
res.status(200);
343+
res.send({});
344+
return next();
345+
});
346+
341347
server.setNextResponse = (response) => {
342348
server._nextResponse = response;
343349
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {
2+
trackUsage,
3+
TestLimitReachedError,
4+
} from '../../../../src/cli/commands/test/iac-local-execution/usage-tracking';
5+
import { mocked } from 'ts-jest/utils';
6+
import { NeedleResponse } from 'needle';
7+
import makeRequest = require('../../../../src/lib/request/request');
8+
import { CustomError } from '../../../../src/lib/errors';
9+
10+
jest.mock('../../../../src/lib/request/request');
11+
const mockedMakeRequest = mocked(makeRequest);
12+
13+
const results = [
14+
{
15+
meta: {
16+
isPrivate: true,
17+
},
18+
result: {
19+
cloudConfigResults: ['an issue'],
20+
},
21+
},
22+
{
23+
meta: {
24+
isPrivate: false,
25+
},
26+
result: {
27+
cloudConfigResults: [],
28+
},
29+
},
30+
];
31+
32+
describe('tracking IaC test usage', () => {
33+
afterEach(() => {
34+
jest.clearAllMocks();
35+
});
36+
37+
it('does not throw an error when backend returns HTTP 200', async () => {
38+
mockedMakeRequest.mockImplementationOnce(() => {
39+
return Promise.resolve({
40+
res: { statusCode: 200 } as NeedleResponse,
41+
body: {
42+
foo: 'bar',
43+
},
44+
});
45+
});
46+
47+
await trackUsage(results);
48+
49+
expect(mockedMakeRequest.mock.calls.length).toEqual(1);
50+
expect(mockedMakeRequest.mock.calls[0][0].body).toEqual({
51+
results: [
52+
{
53+
isPrivate: true,
54+
issuesPrevented: 1,
55+
},
56+
{
57+
isPrivate: false,
58+
issuesPrevented: 0,
59+
},
60+
],
61+
});
62+
});
63+
64+
it('throws TestLimitReachedError when backend returns HTTP 429', async () => {
65+
mockedMakeRequest.mockImplementationOnce(() => {
66+
return Promise.resolve({
67+
res: { statusCode: 429 } as NeedleResponse,
68+
body: {
69+
foo: 'bar',
70+
},
71+
});
72+
});
73+
74+
await expect(trackUsage(results)).rejects.toThrow(
75+
new TestLimitReachedError(),
76+
);
77+
});
78+
79+
it('throws CustomError when backend returns HTTP 500', async () => {
80+
mockedMakeRequest.mockImplementationOnce(() => {
81+
return Promise.resolve({
82+
res: { statusCode: 500, body: { foo: 'bar' } } as NeedleResponse,
83+
body: {
84+
foo: 'bar',
85+
},
86+
});
87+
});
88+
89+
await expect(trackUsage(results)).rejects.toThrow(
90+
new CustomError(
91+
'An error occurred while attempting to track test usage: {"foo":"bar"}',
92+
),
93+
);
94+
});
95+
});

0 commit comments

Comments
 (0)
Please sign in to comment.