From 3ac30a0490aab9171feff315f4ebc45f63e59d5d Mon Sep 17 00:00:00 2001 From: Karl Power Date: Thu, 5 Nov 2026 15:58:40 +0100 Subject: [PATCH] docs(api): document minAutoGranularity on metric and PromQL sources in the v2 spec The v2 sources API already accepted and returned minAutoGranularity via the shared SourceSchema, but the OpenAPI spec omitted it. Clients generated from the spec, including the cloud gateway allow-list, rejected the field. --- .../external-api-min-auto-granularity.md | 10 +++++++ packages/api/openapi.json | 12 ++++++++ .../__tests__/sources-crud.int.test.ts | 29 +++++++++++++++++++ .../src/routers/external-api/v2/sources.ts | 10 +++++++ 4 files changed, 61 insertions(+) create mode 100644 .changeset/external-api-min-auto-granularity.md diff --git a/.changeset/external-api-min-auto-granularity.md b/.changeset/external-api-min-auto-granularity.md new file mode 100644 index 0000000000..c63e6f77be --- /dev/null +++ b/.changeset/external-api-min-auto-granularity.md @@ -0,0 +1,10 @@ +--- +'@hyperdx/api': patch +--- + +docs: Document `minAutoGranularity` on metric and PromQL sources in the external +API + +`GET`, `POST` and `PUT /api/v2/sources` already accepted and returned +`minAutoGranularity`, but the OpenAPI spec omitted it, so clients generated from +the spec dropped or rejected the field. diff --git a/packages/api/openapi.json b/packages/api/openapi.json index 6c2fd20b0f..bffa35d627 100644 --- a/packages/api/openapi.json +++ b/packages/api/openapi.json @@ -5594,6 +5594,12 @@ "description": "HyperDX Source for logs associated with metrics. Optional", "nullable": true, "example": "507f1f77bcf86cd799439011" + }, + "minAutoGranularity": { + "type": "string", + "description": "Floor for the auto-inferred chart granularity. Set it to the scrape/report interval of the metrics so a short time range does not produce buckets smaller than that interval (sparse, steppy series). Only constrains \"Auto\" granularity; an explicit granularity picked on a tile is never overridden. Omit for no minimum. Uses the SQL interval format (a positive integer followed by second, minute, hour, or day), not the short form of the materialized view granularity fields.", + "pattern": "^\\d+ (second|minute|hour|day)$", + "example": "1 minute" } } }, @@ -5736,6 +5742,12 @@ "type": "string", "description": "Required by the API for all source kinds; not used by PromQL queries.", "example": "timestamp" + }, + "minAutoGranularity": { + "type": "string", + "description": "Floor for the auto-inferred chart granularity, and the scrape interval that `$__rate_interval` is computed from. Set it to the Prometheus scrape interval so a short time range does not produce buckets smaller than that interval. Only constrains \"Auto\" granularity; an explicit granularity picked on a tile is never overridden. Omit for no minimum. Uses the SQL interval format (a positive integer followed by second, minute, hour, or day), not the short form of the materialized view granularity fields.", + "pattern": "^\\d+ (second|minute|hour|day)$", + "example": "1 minute" } } }, diff --git a/packages/api/src/routers/external-api/__tests__/sources-crud.int.test.ts b/packages/api/src/routers/external-api/__tests__/sources-crud.int.test.ts index f7286a13c2..7ad19429cd 100644 --- a/packages/api/src/routers/external-api/__tests__/sources-crud.int.test.ts +++ b/packages/api/src/routers/external-api/__tests__/sources-crud.int.test.ts @@ -90,6 +90,16 @@ describe('External API v2 Sources CRUD', () => { connection: connection._id.toString(), }); + const metricSourceBody = () => ({ + kind: SourceKind.Metric, + name: 'Created Metric Source', + from: { databaseName: DEFAULT_DATABASE, tableName: '' }, + timestampValueExpression: 'TimeUnix', + resourceAttributesExpression: 'ResourceAttributes', + metricTables: { gauge: 'otel_metrics_gauge' }, + connection: connection._id.toString(), + }); + const createOtherTeamConnection = (otherTeamId: mongoose.Types.ObjectId) => Connection.create({ team: otherTeamId, @@ -257,6 +267,25 @@ describe('External API v2 Sources CRUD', () => { expect(persisted!.kind).toBe(SourceKind.Promql); }); + it('should round-trip minAutoGranularity in the SQL interval format', async () => { + const response = await authRequest('post', BASE_URL) + .send({ ...metricSourceBody(), minAutoGranularity: '1 minute' }) + .expect(200); + + expect(response.body.data.minAutoGranularity).toBe('1 minute'); + + const persisted = await Source.findById(response.body.data.id); + expect(persisted!.get('minAutoGranularity')).toBe('1 minute'); + }); + + it('should reject a short-form minAutoGranularity', async () => { + // Unlike materializedViews[].minGranularity, this field is not mapped + // from the short form, so the documented contract is the SQL interval. + await authRequest('post', BASE_URL) + .send({ ...metricSourceBody(), minAutoGranularity: '1m' }) + .expect(400); + }); + it('should return 400 when connection is not a valid id', async () => { await authRequest('post', BASE_URL) .send({ ...logSourceBody(), connection: 'my-connection-name' }) diff --git a/packages/api/src/routers/external-api/v2/sources.ts b/packages/api/src/routers/external-api/v2/sources.ts index df25307038..0a30bd7bed 100644 --- a/packages/api/src/routers/external-api/v2/sources.ts +++ b/packages/api/src/routers/external-api/v2/sources.ts @@ -720,6 +720,11 @@ function formatExternalSource(source: SourceDocument) { * description: HyperDX Source for logs associated with metrics. Optional * nullable: true * example: 507f1f77bcf86cd799439011 + * minAutoGranularity: + * type: string + * description: Floor for the auto-inferred chart granularity. Set it to the scrape/report interval of the metrics so a short time range does not produce buckets smaller than that interval (sparse, steppy series). Only constrains "Auto" granularity; an explicit granularity picked on a tile is never overridden. Omit for no minimum. Uses the SQL interval format (a positive integer followed by second, minute, hour, or day), not the short form of the materialized view granularity fields. + * pattern: '^\d+ (second|minute|hour|day)$' + * example: 1 minute * SessionSource: * type: object * required: @@ -830,6 +835,11 @@ function formatExternalSource(source: SourceDocument) { * type: string * description: Required by the API for all source kinds; not used by PromQL queries. * example: timestamp + * minAutoGranularity: + * type: string + * description: Floor for the auto-inferred chart granularity, and the scrape interval that `$__rate_interval` is computed from. Set it to the Prometheus scrape interval so a short time range does not produce buckets smaller than that interval. Only constrains "Auto" granularity; an explicit granularity picked on a tile is never overridden. Omit for no minimum. Uses the SQL interval format (a positive integer followed by second, minute, hour, or day), not the short form of the materialized view granularity fields. + * pattern: '^\d+ (second|minute|hour|day)$' + * example: 1 minute * Source: * oneOf: * - $ref: '#/components/schemas/LogSource'