Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
7.8 kB · 252 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253import type { Attributes, HrTime } from '@opentelemetry/api'import { type ExportResult, ExportResultCode } from '@opentelemetry/core'import { AggregationTemporality, type MetricData, type DataPoint as OTelDataPoint, type PushMetricExporter, type ResourceMetrics,} from '@opentelemetry/sdk-metrics'import axios from 'axios'import { checkMetricsEnabled } from 'src/services/api/metricsOptOut.js'import { getIsNonInteractiveSession } from '../../bootstrap/state.js'import { getSubscriptionType, isClaudeAISubscriber } from '../auth.js'import { checkHasTrustDialogAccepted } from '../config.js'import { logForDebugging } from '../debug.js'import { errorMessage, toError } from '../errors.js'import { getAuthHeaders } from '../http.js'import { logError } from '../log.js'import { jsonStringify } from '../slowOperations.js'import { getClaudeCodeUserAgent } from '../userAgent.js'
type DataPoint = { attributes: Record<string, string> value: number timestamp: string}
type Metric = { name: string description?: string unit?: string data_points: DataPoint[]}
type InternalMetricsPayload = { resource_attributes: Record<string, string> metrics: Metric[]}
export class BigQueryMetricsExporter implements PushMetricExporter { private readonly endpoint: string private readonly timeout: number private pendingExports: Promise<void>[] = [] private isShutdown = false
constructor(options: { timeout?: number } = {}) { const defaultEndpoint = 'https://api.anthropic.com/api/claude_code/metrics'
if ( process.env.USER_TYPE === 'ant' && process.env.ANT_CLAUDE_CODE_METRICS_ENDPOINT ) { this.endpoint = process.env.ANT_CLAUDE_CODE_METRICS_ENDPOINT + '/api/claude_code/metrics' } else { this.endpoint = defaultEndpoint }
this.timeout = options.timeout || 5000 }
async export( metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void, ): Promise<void> { if (this.isShutdown) { resultCallback({ code: ExportResultCode.FAILED, error: new Error('Exporter has been shutdown'), }) return }
const exportPromise = this.doExport(metrics, resultCallback) this.pendingExports.push(exportPromise)
// Clean up completed exports void exportPromise.finally(() => { const index = this.pendingExports.indexOf(exportPromise) if (index > -1) { void this.pendingExports.splice(index, 1) } }) }
private async doExport( metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void, ): Promise<void> { try { // Skip if trust not established in interactive mode // This prevents triggering apiKeyHelper before trust dialog const hasTrust = checkHasTrustDialogAccepted() || getIsNonInteractiveSession() if (!hasTrust) { logForDebugging( 'BigQuery metrics export: trust not established, skipping', ) resultCallback({ code: ExportResultCode.SUCCESS }) return }
// Check organization-level metrics opt-out const metricsStatus = await checkMetricsEnabled() if (!metricsStatus.enabled) { logForDebugging('Metrics export disabled by organization setting') resultCallback({ code: ExportResultCode.SUCCESS }) return }
const payload = this.transformMetricsForInternal(metrics)
const authResult = getAuthHeaders() if (authResult.error) { logForDebugging(`Metrics export failed: ${authResult.error}`) resultCallback({ code: ExportResultCode.FAILED, error: new Error(authResult.error), }) return }
const headers: Record<string, string> = { 'Content-Type': 'application/json', 'User-Agent': getClaudeCodeUserAgent(), ...authResult.headers, }
const response = await axios.post(this.endpoint, payload, { timeout: this.timeout, headers, })
logForDebugging('BigQuery metrics exported successfully') logForDebugging( `BigQuery API Response: ${jsonStringify(response.data, null, 2)}`, ) resultCallback({ code: ExportResultCode.SUCCESS }) } catch (error) { logForDebugging(`BigQuery metrics export failed: ${errorMessage(error)}`) logError(error) resultCallback({ code: ExportResultCode.FAILED, error: toError(error), }) } }
private transformMetricsForInternal( metrics: ResourceMetrics, ): InternalMetricsPayload { const attrs = metrics.resource.attributes
const resourceAttributes: Record<string, string> = { 'service.name': (attrs['service.name'] as string) || 'claude-code', 'service.version': (attrs['service.version'] as string) || 'unknown', 'os.type': (attrs['os.type'] as string) || 'unknown', 'os.version': (attrs['os.version'] as string) || 'unknown', 'host.arch': (attrs['host.arch'] as string) || 'unknown', 'aggregation.temporality': this.selectAggregationTemporality() === AggregationTemporality.DELTA ? 'delta' : 'cumulative', }
// Only add wsl.version if it exists (omit instead of default) if (attrs['wsl.version']) { resourceAttributes['wsl.version'] = attrs['wsl.version'] as string }
// Add customer type and subscription type if (isClaudeAISubscriber()) { resourceAttributes['user.customer_type'] = 'claude_ai' const subscriptionType = getSubscriptionType() if (subscriptionType) { resourceAttributes['user.subscription_type'] = subscriptionType } } else { resourceAttributes['user.customer_type'] = 'api' }
const transformed = { resource_attributes: resourceAttributes, metrics: metrics.scopeMetrics.flatMap(scopeMetric => scopeMetric.metrics.map(metric => ({ name: metric.descriptor.name, description: metric.descriptor.description, unit: metric.descriptor.unit, data_points: this.extractDataPoints(metric), })), ), }
return transformed }
private extractDataPoints(metric: MetricData): DataPoint[] { const dataPoints = metric.dataPoints || []
return dataPoints .filter( (point): point is OTelDataPoint<number> => typeof point.value === 'number', ) .map(point => ({ attributes: this.convertAttributes(point.attributes), value: point.value, timestamp: this.hrTimeToISOString( point.endTime || point.startTime || [Date.now() / 1000, 0], ), })) }
async shutdown(): Promise<void> { this.isShutdown = true await this.forceFlush() logForDebugging('BigQuery metrics exporter shutdown complete') }
async forceFlush(): Promise<void> { await Promise.all(this.pendingExports) logForDebugging('BigQuery metrics exporter flush complete') }
private convertAttributes( attributes: Attributes | undefined, ): Record<string, string> { const result: Record<string, string> = {} if (attributes) { for (const [key, value] of Object.entries(attributes)) { if (value !== undefined && value !== null) { result[key] = String(value) } } } return result }
private hrTimeToISOString(hrTime: HrTime): string { const [seconds, nanoseconds] = hrTime const date = new Date(seconds * 1000 + nanoseconds / 1000000) return date.toISOString() }
selectAggregationTemporality(): AggregationTemporality { // DO NOT CHANGE THIS TO CUMULATIVE // It would mess up the aggregation of metrics // for CC Productivity metrics dashboard return AggregationTemporality.DELTA }}