@knapsack-pro/core
Version:
Knapsack Pro Core library splits tests across CI nodes and makes sure that tests will run in optimal time on each CI node. This library gives core features like communication with KnapsackPro.com API. This library is a dependency for other projects specif
149 lines (148 loc) • 6.23 kB
JavaScript
import axios from 'axios';
import axiosRetry, { retryAfter } from 'axios-retry';
import { v4 as uuidv4 } from 'uuid';
import { KnapsackProEnvConfig, buildAuthor, commitAuthors, ciProvider, } from './config/index.js';
import { logDiagnostics } from './diagnostics.js';
import { KnapsackProLogger } from './knapsack-pro-logger.js';
export const getHeaders = ({ clientName, clientVersion, }) => {
const ci = ciProvider();
return {
'KNAPSACK-PRO-CLIENT-NAME': clientName,
'KNAPSACK-PRO-CLIENT-VERSION': clientVersion,
...(ci !== null ? { 'KNAPSACK-PRO-CI-PROVIDER': ci } : {}),
};
};
export class KnapsackProAPI {
api;
knapsackProLogger;
constructor(clientName, clientVersion) {
this.knapsackProLogger = new KnapsackProLogger();
this.api = this.setUpApiClient(clientName, clientVersion);
}
fetchTestsFromQueue(allTestFiles, initializeQueue, attemptConnectToQueue) {
const url = '/v1/queues/queue';
const data = {
test_suite_token: KnapsackProEnvConfig.testSuiteToken,
can_initialize_queue: initializeQueue,
attempt_connect_to_queue: attemptConnectToQueue,
fixed_queue_split: KnapsackProEnvConfig.fixedQueueSplit,
commit_hash: KnapsackProEnvConfig.commitHash,
branch: KnapsackProEnvConfig.branch,
node_total: KnapsackProEnvConfig.ciNodeTotal,
node_index: KnapsackProEnvConfig.ciNodeIndex,
node_build_id: KnapsackProEnvConfig.ciNodeBuildId,
user_seat: KnapsackProEnvConfig.maskedUserSeat,
batch_uuid: uuidv4(),
...(initializeQueue &&
!attemptConnectToQueue && {
test_files: allTestFiles,
build_author: buildAuthor(),
commit_authors: commitAuthors(),
}),
};
return this.api.post(url, data);
}
createBuildSubset(recordedTestFiles) {
const url = '/v1/build_subsets';
const data = {
test_suite_token: KnapsackProEnvConfig.testSuiteToken,
commit_hash: KnapsackProEnvConfig.commitHash,
branch: KnapsackProEnvConfig.branch,
node_total: KnapsackProEnvConfig.ciNodeTotal,
node_index: KnapsackProEnvConfig.ciNodeIndex,
test_files: recordedTestFiles,
};
return this.api.post(url, data);
}
isExpectedErrorStatus(error) {
const { response } = error;
if (!response) {
return false;
}
const { status } = response;
return (status === 400 ||
status === 422 ||
status === 403);
}
setUpApiClient(clientName, clientVersion) {
const apiClient = axios.create({
baseURL: KnapsackProEnvConfig.endpoint,
timeout: 15000,
headers: getHeaders({ clientName, clientVersion }),
});
axiosRetry(apiClient, {
retries: 3,
shouldResetTimeout: true,
retryDelay: this.retryDelay,
retryCondition: this.retryCondition,
onMaxRetryTimesExceeded: this.onMaxRetryTimesExceeded,
onRetry: this.onRetry,
});
apiClient.interceptors.request.use((config) => {
const { method, baseURL, url, headers, data } = config;
const apiUrl = baseURL + url.replace(baseURL ?? '', '');
const requestHeaders = KnapsackProLogger.objectInspect(headers);
const requestBody = KnapsackProLogger.objectInspect(data);
this.knapsackProLogger.info(`${method?.toUpperCase()} ${apiUrl}`);
this.knapsackProLogger.debug(`${method?.toUpperCase()} ${apiUrl}\n\n` +
'Request headers:\n' +
`${requestHeaders}\n\n` +
'Request body:\n' +
`${requestBody}`);
return config;
});
apiClient.interceptors.response.use((response) => {
const { status, statusText, data, headers: { 'x-request-id': requestId }, } = response;
const responeseBody = KnapsackProLogger.objectInspect(data);
this.knapsackProLogger.info(`${status} ${statusText}\n\n` +
'Request ID:\n' +
`${requestId}\n\n` +
'Response body:\n' +
`${responeseBody}`);
return response;
}, (error) => {
const { response } = error;
if (response) {
const { status, statusText, data, headers: { 'x-request-id': requestId }, } = response;
const responeseBody = KnapsackProLogger.objectInspect(data);
this.knapsackProLogger.error(`${status} ${statusText}\n\n` +
'Request ID:\n' +
`${requestId}\n\n` +
'Response error body:\n' +
`${responeseBody}`);
}
else {
this.knapsackProLogger.error(error);
}
return Promise.reject(error);
});
return apiClient;
}
retryCondition = (error) => {
return (axiosRetry.isNetworkError(error) ||
this.isRetriableRequestError(error) ||
!this.isExpectedErrorStatus(error));
};
isRetriableRequestError(error) {
if (!error.config) {
return false;
}
return axiosRetry.isRetryableError(error);
}
retryDelay = (retryCount, error) => {
let delay = retryAfter(error);
if (delay === 0) {
const base = 0.17 * Math.pow(6, retryCount);
const min = base * 0.75;
const jitter = base * 0.25 * Math.random();
delay = (min + jitter) * 1000;
}
this.knapsackProLogger.warn(`(${retryCount}) Retrying request to Knapsack Pro in ${Number(delay.toFixed())} ms...`);
return delay;
};
onMaxRetryTimesExceeded = (_error, _retryCount) => logDiagnostics(this.knapsackProLogger, KnapsackProEnvConfig.endpoint);
onRetry = (retryCount, _error, requestConfig) => {
requestConfig.headers = requestConfig.headers ?? {};
requestConfig.headers['X-Retry-Count'] = retryCount;
};
}