Skip to main content
Glama
metrics-client.ts14.5 kB
/** * @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, MetricDirection, MetricThresholdStatus, RepositoryMetric, UpdateMetricThresholdParams, UpdateMetricSettingParams, MetricThresholdUpdateResponse, MetricSettingUpdateResponse, MetricHistoryParams, MetricHistoryResponse, } 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: string, options: { shortcodeIn?: MetricShortcode[] } = {} ): Promise<RepositoryMetric[]> { 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: UpdateMetricThresholdParams ): Promise<MetricThresholdUpdateResponse> { 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: UpdateMetricSettingParams ): Promise<MetricSettingUpdateResponse> { 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: MetricHistoryParams): Promise<MetricHistoryResponse | null> { 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 */ private static buildQualityMetricsQuery(): string { 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 */ private static buildUpdateThresholdMutation(): string { 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 */ private static buildUpdateSettingMutation(): string { 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 */ private static buildMetricHistoryQuery(): string { 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 */ private extractMetricsFromResponse(responseData: unknown): RepositoryMetric[] { const metrics: RepositoryMetric[] = []; try { const repository = (responseData as Record<string, unknown>)?.repository as Record< string, unknown >; const repositoryMetrics = (repository?.metrics ?? []) as Array<Record<string, unknown>>; for (const metric of repositoryMetrics) { const items = (metric.items ?? []) as Array<Record<string, unknown>>; metrics.push({ shortcode: String(metric.shortcode ?? '') as MetricShortcode, name: String(metric.name ?? ''), description: String(metric.description ?? ''), positiveDirection: String(metric.direction ?? 'UPWARD') as MetricDirection, 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') as MetricThresholdStatus, })), }); } } catch (error) { this.logger.error('Error extracting metrics from response', { error }); } return metrics; } /** * Extracts history data from GraphQL response * @private */ private extractHistoryFromResponse( responseData: unknown, params: MetricHistoryParams ): MetricHistoryResponse | null { try { const repository = (responseData as Record<string, unknown>)?.repository as Record< string, unknown >; const metrics = (repository?.metrics ?? []) as Array<Record<string, unknown>>; const metricData = metrics.find((m) => m.shortcode === params.metricShortcode); if (!metricData) { return null; } const items = (metricData.items ?? []) as Array<Record<string, unknown>>; const itemData = items.find((i) => i.key === params.metricKey); if ( !itemData || !('values' in itemData) || !(itemData.values as Record<string, unknown>)?.edges ) { return null; } const values = []; const edges = ((itemData.values as Record<string, unknown>).edges ?? []) as Array< Record<string, unknown> >; for (const edge of edges) { if (!edge.node) continue; const node = edge.node as Record<string, unknown>; 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' as MetricDirection, 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 */ private static calculateTrend( values: Array<{ value: number; createdAt: string }> ): 'improving' | 'declining' | 'stable' { 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 */ private static handleTestEnvironment( params: MetricHistoryParams ): MetricHistoryResponse | null | undefined { 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' as MetricDirection, 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 */ private handleMetricsError(error: unknown): RepositoryMetric[] { this.logger.error('Error in getQualityMetrics', { errorType: typeof error, errorMessage: error instanceof Error ? error.message : String(error), }); if (isErrorWithMessage(error, 'NoneType')) { return []; } throw error; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sapientpants/deepsource-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server