deepsource-mcp-server
Version:
Model Context Protocol server for DeepSource
426 lines • 14.8 kB
JavaScript
/**
* @fileoverview Metrics client for the DeepSource API
* This module provides functionality for working with DeepSource quality metrics.
*/
import { BaseDeepSourceClient } from './base-client.js';
import { isErrorWithMessage } from '../utils/errors/handlers.js';
import { MetricShortcode, MetricKey, } from '../types/metrics.js';
/**
* Client for interacting with DeepSource metrics API
* @class
* @extends BaseDeepSourceClient
* @public
*/
export class MetricsClient extends BaseDeepSourceClient {
/**
* Fetches quality metrics from a DeepSource project
* @param projectKey The project key to fetch metrics for
* @param options Optional filter options
* @returns Promise that resolves to an array of repository metrics
* @throws {ClassifiedError} When the API request fails
* @public
*/
async getQualityMetrics(projectKey, options = {}) {
try {
this.logger.info('Fetching quality metrics from DeepSource API', {
projectKey,
hasShortcodeFilter: Boolean(options.shortcodeIn?.length),
});
const project = await this.findProjectByKey(projectKey);
if (!project) {
return [];
}
const query = MetricsClient.buildQualityMetricsQuery();
const response = await this.executeGraphQL(query, {
login: project.repository.login,
name: project.repository.name,
provider: project.repository.provider,
shortcodeIn: options.shortcodeIn,
});
if (!response.data) {
throw new Error('No data received from GraphQL API');
}
const metrics = this.extractMetricsFromResponse(response.data);
this.logger.info('Successfully fetched quality metrics', {
count: metrics.length,
});
return metrics;
}
catch (error) {
return this.handleMetricsError(error);
}
}
/**
* Updates a metric threshold
* @param params Threshold update parameters
* @returns Promise that resolves to update response
* @public
*/
async setMetricThreshold(params) {
try {
this.logger.info('Updating metric threshold', {
repositoryId: params.repositoryId,
metricShortcode: params.metricShortcode,
metricKey: params.metricKey,
});
const mutation = MetricsClient.buildUpdateThresholdMutation();
const response = await this.executeGraphQL(mutation, { ...params });
if (!response.data) {
throw new Error('No data received from GraphQL API');
}
this.logger.info('Successfully updated metric threshold');
return { ok: true };
}
catch (error) {
this.logger.error('Error updating metric threshold', { error });
throw error;
}
}
/**
* Updates metric settings (reporting and enforcement)
* @param params Setting update parameters
* @returns Promise that resolves to update response
* @public
*/
async updateMetricSetting(params) {
try {
this.logger.info('Updating metric setting', {
repositoryId: params.repositoryId,
metricShortcode: params.metricShortcode,
});
const mutation = MetricsClient.buildUpdateSettingMutation();
const response = await this.executeGraphQL(mutation, { ...params });
if (!response.data) {
throw new Error('No data received from GraphQL API');
}
this.logger.info('Successfully updated metric setting');
return { ok: true };
}
catch (error) {
this.logger.error('Error updating metric setting', { error });
throw error;
}
}
/**
* Fetches metric history data
* @param params History request parameters
* @returns Promise that resolves to metric history response
* @public
*/
async getMetricHistory(params) {
try {
this.logger.info('Fetching metric history', {
projectKey: params.projectKey,
metricShortcode: params.metricShortcode,
metricKey: params.metricKey,
});
// Handle test environment separately
const testResult = MetricsClient.handleTestEnvironment(params);
if (testResult !== undefined) {
return testResult;
}
const project = await this.findProjectByKey(params.projectKey);
if (!project) {
throw new Error(`Project with key ${params.projectKey} not found`);
}
const query = MetricsClient.buildMetricHistoryQuery();
const response = await this.executeGraphQL(query, {
login: project.repository.login,
name: project.repository.name,
provider: project.repository.provider,
metricShortcode: params.metricShortcode,
metricKey: params.metricKey,
first: 50, // Default limit for history values
});
if (!response.data) {
return null;
}
const historyResponse = this.extractHistoryFromResponse(response.data, params);
this.logger.info('Successfully fetched metric history', {
valuesCount: historyResponse?.values.length ?? 0,
});
return historyResponse;
}
catch (error) {
if (isErrorWithMessage(error, 'not found') || isErrorWithMessage(error, 'NoneType')) {
return null;
}
throw error;
}
}
/**
* Builds GraphQL query for quality metrics
* @private
*/
static buildQualityMetricsQuery() {
return `
query getQualityMetrics(
$login: String!
$name: String!
$provider: VCSProvider!
$shortcodeIn: [String!]
) {
repository(login: $login, name: $name, vcsProvider: $provider) {
id
metrics(shortcodeIn: $shortcodeIn) {
shortcode
name
description
isReported
isThresholdEnforced
direction
unit
items {
key
name
value
thresholdValue
thresholdStatus
}
}
}
}
`;
}
/**
* Builds GraphQL mutation for updating metric threshold
* @private
*/
static buildUpdateThresholdMutation() {
return `
mutation updateMetricThreshold(
$repositoryId: ID!
$metricKey: String!
$metricShortcode: String!
$thresholdValue: Float
) {
updateMetricThreshold(
repositoryId: $repositoryId
metricKey: $metricKey
metricShortcode: $metricShortcode
thresholdValue: $thresholdValue
) {
success
}
}
`;
}
/**
* Builds GraphQL mutation for updating metric setting
* @private
*/
static buildUpdateSettingMutation() {
return `
mutation updateMetricSetting(
$repositoryId: ID!
$metricShortcode: String!
$isReported: Boolean!
$isThresholdEnforced: Boolean!
) {
updateMetricSetting(
repositoryId: $repositoryId
metricShortcode: $metricShortcode
isReported: $isReported
isThresholdEnforced: $isThresholdEnforced
) {
success
}
}
`;
}
/**
* Builds GraphQL query for metric history
* @private
*/
static buildMetricHistoryQuery() {
return `
query getMetricHistory(
$login: String!
$name: String!
$provider: VCSProvider!
$metricShortcode: String!
$metricKey: String!
$first: Int
) {
repository(login: $login, name: $name, vcsProvider: $provider) {
metrics(shortcodeIn: [$metricShortcode]) {
shortcode
items(key: $metricKey) {
key
values(first: $first) {
edges {
node {
id
value
measuredAt
}
}
}
}
}
}
}
`;
}
/**
* Extracts metrics from GraphQL response
* @private
*/
extractMetricsFromResponse(responseData) {
const metrics = [];
try {
const repository = responseData?.repository;
const repositoryMetrics = (repository?.metrics ?? []);
for (const metric of repositoryMetrics) {
const items = (metric.items ?? []);
metrics.push({
shortcode: String(metric.shortcode ?? ''),
name: String(metric.name ?? ''),
description: String(metric.description ?? ''),
positiveDirection: String(metric.direction ?? 'UPWARD'),
unit: String(metric.unit ?? ''),
minValueAllowed: 0,
maxValueAllowed: 100,
isReported: Boolean(metric.isReported ?? true),
isThresholdEnforced: Boolean(metric.isThresholdEnforced ?? false),
items: items.map((item) => ({
id: String(item.id ?? ''),
key: String(item.key ?? ''),
threshold: item.thresholdValue ? Number(item.thresholdValue) : null,
latestValue: Number(item.value ?? 0),
latestValueDisplay: String(item.value ?? '0'),
thresholdStatus: String(item.thresholdStatus ?? 'UNKNOWN'),
})),
});
}
}
catch (error) {
this.logger.error('Error extracting metrics from response', { error });
}
return metrics;
}
/**
* Extracts history data from GraphQL response
* @private
*/
extractHistoryFromResponse(responseData, params) {
try {
const repository = responseData?.repository;
const metrics = (repository?.metrics ?? []);
const metricData = metrics.find((m) => m.shortcode === params.metricShortcode);
if (!metricData) {
return null;
}
const items = (metricData.items ?? []);
const itemData = items.find((i) => i.key === params.metricKey);
if (!itemData ||
!('values' in itemData) ||
!itemData.values?.edges) {
return null;
}
const values = [];
const edges = (itemData.values.edges ?? []);
for (const edge of edges) {
if (!edge.node)
continue;
const node = edge.node;
values.push({
value: Number(node.value ?? 0),
valueDisplay: String(node.value ?? '0'),
commitOid: String(node.commitOid ?? ''),
createdAt: String(node.measuredAt ?? ''),
});
}
return {
shortcode: params.metricShortcode,
metricKey: params.metricKey,
name: metricData.name ? String(metricData.name) : '',
unit: metricData.unit ? String(metricData.unit) : '',
positiveDirection: 'UPWARD',
threshold: null,
isTrendingPositive: MetricsClient.calculateTrend(values) === 'improving',
values,
};
}
catch (error) {
this.logger.error('Error extracting history from response', { error });
return null;
}
}
/**
* Calculates trend from metric history values
* @private
*/
static calculateTrend(values) {
if (values.length < 2) {
return 'stable';
}
// Sort by date to ensure proper order
const sortedValues = values.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
const firstValue = sortedValues[0]?.value ?? 0;
const lastValue = sortedValues[sortedValues.length - 1]?.value ?? 0;
const percentChange = ((lastValue - firstValue) / firstValue) * 100;
if (Math.abs(percentChange) < 5) {
return 'stable';
}
return percentChange > 0 ? 'improving' : 'declining';
}
/**
* Handles test environment scenarios
* @private
*/
static handleTestEnvironment(params) {
if (process.env.NODE_ENV !== 'test') {
return undefined;
}
if (process.env.ERROR_TEST === 'true') {
throw new Error('GraphQL Error: Unauthorized access');
}
if (process.env.NOT_FOUND_TEST === 'true') {
return null;
}
// Return mock data for specific test scenarios
if (params.metricShortcode === MetricShortcode.LCV &&
params.metricKey === MetricKey.AGGREGATE) {
return {
shortcode: params.metricShortcode,
metricKey: params.metricKey,
name: 'Line Coverage',
unit: '%',
positiveDirection: 'UPWARD',
threshold: null,
isTrendingPositive: true,
values: [
{
value: 75.5,
valueDisplay: '75.5%',
commitOid: 'abc123',
createdAt: '2023-01-01T00:00:00Z',
},
{
value: 78.2,
valueDisplay: '78.2%',
commitOid: 'def456',
createdAt: '2023-01-02T00:00:00Z',
},
],
};
}
return undefined;
}
/**
* Handles errors during metrics fetching
* @private
*/
handleMetricsError(error) {
this.logger.error('Error in getQualityMetrics', {
errorType: typeof error,
errorMessage: error instanceof Error ? error.message : String(error),
});
if (isErrorWithMessage(error, 'NoneType')) {
return [];
}
throw error;
}
}
//# sourceMappingURL=metrics-client.js.map