import { AnalyticService, InsightsService, ObservabilityService } from '@sentio/api' import { QuerySQLResultRequestSchema, QuerySQLResultResponseSchema, SQLRequestSchema, SyncExecuteSQLResponseSchema, AsyncExecuteSQLResponseSchema } from '@sentio/api/gen/service/analytic/protos/analytic_service_pb.js' import { QueryRequestSchema, QueryResponseSchema } from '@sentio/api/gen/service/insights/protos/insights_service_pb.js' import { GetMetricsRequestSchema, GetMetricsResponseSchema } from '@sentio/api/gen/service/observability/protos/observability_service_pb.js' import { fromJson, toJson } from '@bufbuild/protobuf' import { Command, InvalidArgumentError } from '@commander-js/extra-typings' import chalk from 'chalk' import process from 'process' import yaml from 'yaml' import { ApiContext, CliError, createApiContext, fetchApiJson, getServiceClient, handleCommandError, loadJsonInput, resolveProjectRef } from '../api.js' const DEFAULT_RANGE_STEP = 3600 // Valid function names sourced from sentio/app/lib/functions.ts const VALID_METRIC_FUNCTIONS: ReadonlySet = new Set([ // Math 'abs', 'ceil', 'floor', 'round', 'log2', 'log10', 'ln', // Rollup 'rollup_avg', 'rollup_count', 'rollup_last', 'rollup_max', 'rollup_min', 'rollup_sum', 'rollup_delta', // Aggregate Over Time 'avg_over_time', 'count_over_time', 'last_over_time', 'max_over_time', 'min_over_time', 'sum_over_time', 'delta_over_time', // Rate 'rate', 'irate', 'delta', 'moving_delta', // Rank 'topk', 'bottomk', // Time 'timestamp', 'day_of_year', 'day_of_month', 'day_of_week', 'year', 'month', 'hour', 'minute', // TimeShift 'before', 'after' ]) const VALID_EVENT_FUNCTIONS: ReadonlySet = new Set([ // Rank 'topk', 'bottomk', // Delta 'delta' ]) interface CommonDataOptions { host?: string apiKey?: string token?: string project?: string owner?: string name?: string projectId?: string file?: string stdin?: boolean json?: boolean yaml?: boolean } interface RangeOptions { start?: string end?: string step?: number timezone?: string limit?: number offset?: number version?: number } interface DataQueryOptions extends CommonDataOptions, RangeOptions { doc?: boolean event?: string metric?: string price?: string[] alias?: string sourceName?: string filter?: string[] groupBy?: string[] aggr?: string func?: string[] } interface SqlCommandOptions extends CommonDataOptions { query?: string result?: string cursor?: string async?: boolean version?: number engine?: string noCache?: boolean cacheTtlSecs?: number cacheRefreshTtlSecs?: number } export function createDataCommand() { const dataCommand = new Command('data').description('Retrieve data from Sentio') dataCommand.addCommand(createDataQueryCommand()) dataCommand.addCommand(createDataEventsCommand()) dataCommand.addCommand(createDataMetricsCommand()) dataCommand.addCommand(createDataSqlCommand()) return dataCommand } export function buildTimeRangeLite(options: RangeOptions) { return { start: options.start ?? 'now-7d', end: options.end ?? 'now', step: options.step ?? DEFAULT_RANGE_STEP, timezone: options.timezone } } export function buildEventsInsightQueryBody( search: string, options: RangeOptions & { alias?: string sourceName?: string filter?: string[] groupBy?: string[] aggr?: string func?: string[] } ) { return { timeRange: buildTimeRangeLite(options), limit: options.limit, offset: options.offset, version: options.version, queries: [ { dataSource: 'EVENTS', sourceName: options.sourceName ?? '', eventsQuery: { id: 'events', alias: options.alias ?? search, resource: { type: 'EVENTS', name: search }, aggregation: buildEventAggregation(options.aggr), selectorExpr: buildSelectorExpr(options.filter), groupBy: normalizeListOption(options.groupBy), functions: buildFunctions(options.func, VALID_EVENT_FUNCTIONS), disabled: false } } ], formulas: [] } } export function buildMetricsInsightQueryBody( query: string, options: RangeOptions & { alias?: string sourceName?: string filter?: string[] groupBy?: string[] aggr?: string func?: string[] } ) { return { timeRange: buildTimeRangeLite(options), limit: options.limit, offset: options.offset, version: options.version, queries: [ { dataSource: 'METRICS', sourceName: options.sourceName ?? '', metricsQuery: { id: 'metrics', alias: options.alias ?? query, query, labelSelector: buildMetricLabelSelector(options.filter), aggregate: buildMetricAggregate(options.aggr, options.groupBy), functions: buildFunctions(options.func, VALID_METRIC_FUNCTIONS), disabled: false } } ], formulas: [] } } export function buildPriceInsightQueryBody( prices: string[], options: RangeOptions & { alias?: string sourceName?: string } ) { const normalizedPrices = normalizeListOption(prices) if (normalizedPrices.length === 0) { throw new CliError('Price query requires at least one coin id.') } return { timeRange: buildTimeRangeLite(options), limit: options.limit, offset: options.offset, version: options.version, queries: [ { dataSource: 'PRICE', sourceName: options.sourceName ?? '', priceQuery: { id: 'price', alias: options.alias ?? normalizedPrices.join(','), coinId: normalizedPrices.map(parseCoinIdentifier), disabled: false } } ], formulas: [] } } export function buildDataQueryBody(options: DataQueryOptions) { const selectedKinds = [ Boolean(options.event), Boolean(options.metric), normalizeListOption(options.price).length > 0 ].filter(Boolean).length if (selectedKinds > 1) { throw new CliError('Use exactly one of --event, --metric, or --price for data query.') } const priceValues = normalizeListOption(options.price) if (priceValues.length > 0) { if ( normalizeListOption(options.filter).length > 0 || normalizeListOption(options.groupBy).length > 0 || options.aggr || normalizeListOption(options.func).length > 0 ) { throw new CliError('Price queries only support --price, --alias, --source-name, and time range options.') } return buildPriceInsightQueryBody(priceValues, options) } if (options.metric) { return buildMetricsInsightQueryBody(options.metric, options) } if (!options.event) { return undefined } return buildEventsInsightQueryBody(options.event, options) } export function buildSqlExecuteBody(options: SqlCommandOptions) { const cachePolicy = options.noCache || options.cacheTtlSecs !== undefined || options.cacheRefreshTtlSecs !== undefined ? { noCache: options.noCache ?? false, cacheTtlSecs: options.cacheTtlSecs, cacheRefreshTtlSecs: options.cacheRefreshTtlSecs } : undefined const body: Record = { version: options.version, cursor: options.cursor, sqlQuery: { sql: options.query } } if (cachePolicy) { body.cachePolicy = cachePolicy } if (options.engine) { body.engine = options.engine } return body } function createDataEventsCommand() { return withOutputOptions( withSharedProjectOptions(withAuthOptions(new Command('events').description('List available events for a project'))) ) .option('--version ', 'Processor version', parseInteger) .action(async (options, command) => { try { await runEventsList(options) } catch (error) { handleDataCommandError(error, command) } }) } function createDataQueryCommand() { return withOutputOptions( withQueryInputOptions( withSharedProjectOptions(withAuthOptions(new Command('query').description('Query data with Sentio insights'))) ) ) .option('--doc', 'Print the full insights query file format and exit') .option('--event ', 'Event name to query when not using --file/--stdin') .option('--metric ', 'Metric name to query when not using --file/--stdin') .option('--price ', 'Coin id for a price query, for example ETH or 1:0xabc...', collectOption, []) .option('--alias ', 'Alias for generated query') .option('--source-name ', 'Optional source name for generated query') .option( '--filter ', 'Event filter or metric label selector like field:value or chain=1', collectOption, [] ) .option('--group-by ', 'Group by event property or metric label', collectOption, []) .option('--aggr ', 'Event: total|unique|AAU|DAU|WAU|MAU. Metric: avg|sum|min|max|count') .option('--func ', 'Function like topk(1), bottomk(1), delta(1m)', collectOption, []) .option('--start