From 42b411eed858670a58f9160ab42f73521ba5ebe0 Mon Sep 17 00:00:00 2001 From: Alex Fedotyev <61838744+alex-fedotyev@users.noreply.github.com> Date: Wed, 10 Jun 2026 03:22:54 +0000 Subject: [PATCH 1/3] feat(sources): add an optional Section field to data sources Add a `section` field to the shared source schema, persist it on the Mongoose source model, expose it as an input on the source settings form, and document it on the read-only GET /api/v2/sources surface (OpenAPI plus a round-trip test). The field is optional, so sources without one keep today's behavior. Co-Authored-By: Claude Opus 4.7 --- .changeset/source-section-field.md | 12 +++++ packages/api/openapi.json | 28 +++++++++++ packages/api/src/models/source.ts | 1 + .../external-api/__tests__/sources.test.ts | 48 +++++++++++++++++++ .../src/routers/external-api/v2/sources.ts | 24 ++++++++++ .../app/src/components/Sources/SourceForm.tsx | 7 +++ packages/common-utils/src/types.ts | 6 +++ 7 files changed, 126 insertions(+) create mode 100644 .changeset/source-section-field.md diff --git a/.changeset/source-section-field.md b/.changeset/source-section-field.md new file mode 100644 index 0000000000..e37cc5c64d --- /dev/null +++ b/.changeset/source-section-field.md @@ -0,0 +1,12 @@ +--- +"@hyperdx/common-utils": patch +"@hyperdx/api": patch +"@hyperdx/app": patch +--- + +feat: add an optional Section field to data sources + +Sources can now carry an optional free-text Section label, set from the source +settings form. The value is persisted and returned by GET /api/v2/sources, so +external API consumers can read it. This lays the groundwork for grouping and +searching sources by section in the source selector. diff --git a/packages/api/openapi.json b/packages/api/openapi.json index 9bcad76117..169529c45e 100644 --- a/packages/api/openapi.json +++ b/packages/api/openapi.json @@ -2951,6 +2951,13 @@ "description": "Display name for the source.", "example": "Logs" }, + "section": { + "type": "string", + "maxLength": 256, + "description": "Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together.", + "nullable": true, + "example": "Billing" + }, "kind": { "type": "string", "enum": [ @@ -3139,6 +3146,13 @@ "description": "Display name for the source.", "example": "Traces" }, + "section": { + "type": "string", + "maxLength": 256, + "description": "Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together.", + "nullable": true, + "example": "Billing" + }, "kind": { "type": "string", "enum": [ @@ -3354,6 +3368,13 @@ "description": "Display name for the source.", "example": "Metrics" }, + "section": { + "type": "string", + "maxLength": 256, + "description": "Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together.", + "nullable": true, + "example": "Billing" + }, "kind": { "type": "string", "enum": [ @@ -3422,6 +3443,13 @@ "description": "Display name for the source.", "example": "Sessions" }, + "section": { + "type": "string", + "maxLength": 256, + "description": "Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together.", + "nullable": true, + "example": "Billing" + }, "kind": { "type": "string", "enum": [ diff --git a/packages/api/src/models/source.ts b/packages/api/src/models/source.ts index b7e7d55d33..7623af007b 100644 --- a/packages/api/src/models/source.ts +++ b/packages/api/src/models/source.ts @@ -91,6 +91,7 @@ const sourceBaseSchema = new Schema( ref: 'Connection', }, name: String, + section: String, disabled: { type: Boolean, default: false, diff --git a/packages/api/src/routers/external-api/__tests__/sources.test.ts b/packages/api/src/routers/external-api/__tests__/sources.test.ts index 91a7296ff0..dfe3d59494 100644 --- a/packages/api/src/routers/external-api/__tests__/sources.test.ts +++ b/packages/api/src/routers/external-api/__tests__/sources.test.ts @@ -533,6 +533,54 @@ describe('External API v2 Sources', () => { expect(response.body.data).toHaveLength(1); expect(response.body.data[0].id).toBe(validSource._id.toString()); }); + + describe('section field', () => { + const SECTION = 'Control Plane Prod'; + + it('returns the section on a source that has one', async () => { + const logSource = await LogSource.create({ + kind: SourceKind.Log, + team: team._id, + name: 'Sectioned Log Source', + section: SECTION, + from: { + databaseName: DEFAULT_DATABASE, + tableName: DEFAULT_LOGS_TABLE, + }, + timestampValueExpression: 'Timestamp', + defaultTableSelectExpression: '*', + connection: connection._id, + }); + + const response = await authRequest('get', BASE_URL).expect(200); + + expect(response.body.data).toHaveLength(1); + expect(response.body.data[0]).toMatchObject({ + id: logSource._id.toString(), + section: SECTION, + }); + }); + + it('omits the section on a source that has none', async () => { + await LogSource.create({ + kind: SourceKind.Log, + team: team._id, + name: 'Unsectioned Log Source', + from: { + databaseName: DEFAULT_DATABASE, + tableName: DEFAULT_LOGS_TABLE, + }, + timestampValueExpression: 'Timestamp', + defaultTableSelectExpression: '*', + connection: connection._id, + }); + + const response = await authRequest('get', BASE_URL).expect(200); + + expect(response.body.data).toHaveLength(1); + expect(response.body.data[0]).not.toHaveProperty('section'); + }); + }); }); describe('backward compatibility with legacy flat-model documents', () => { diff --git a/packages/api/src/routers/external-api/v2/sources.ts b/packages/api/src/routers/external-api/v2/sources.ts index 0903dccf0e..8cf022100f 100644 --- a/packages/api/src/routers/external-api/v2/sources.ts +++ b/packages/api/src/routers/external-api/v2/sources.ts @@ -274,6 +274,12 @@ function formatExternalSource(source: SourceDocument) { * type: string * description: Display name for the source. * example: Logs + * section: + * type: string + * maxLength: 256 + * description: Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together. + * nullable: true + * example: Billing * kind: * type: string * enum: [log] @@ -421,6 +427,12 @@ function formatExternalSource(source: SourceDocument) { * type: string * description: Display name for the source. * example: Traces + * section: + * type: string + * maxLength: 256 + * description: Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together. + * nullable: true + * example: Billing * kind: * type: string * enum: [trace] @@ -589,6 +601,12 @@ function formatExternalSource(source: SourceDocument) { * type: string * description: Display name for the source. * example: Metrics + * section: + * type: string + * maxLength: 256 + * description: Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together. + * nullable: true + * example: Billing * kind: * type: string * enum: [metric] @@ -641,6 +659,12 @@ function formatExternalSource(source: SourceDocument) { * type: string * description: Display name for the source. * example: Sessions + * section: + * type: string + * maxLength: 256 + * description: Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together. + * nullable: true + * example: Billing * kind: * type: string * enum: [session] diff --git a/packages/app/src/components/Sources/SourceForm.tsx b/packages/app/src/components/Sources/SourceForm.tsx index 6c4043afb1..e81facf11d 100644 --- a/packages/app/src/components/Sources/SourceForm.tsx +++ b/packages/app/src/components/Sources/SourceForm.tsx @@ -2447,6 +2447,13 @@ export function TableSourceForm({ rules={{ required: 'Name is required' }} /> + + + Date: Wed, 10 Jun 2026 04:05:55 +0000 Subject: [PATCH 2/3] fix(sources): address review feedback on #2432 - OpenAPI: drop `nullable: true` from the `section` field on all four source schemas. The field is optional (string or absent), never null, so the spec should not advertise null to API consumers. - Form: add `maxLength={256}` to the Section input so the 256-char cap from the schema is enforced client-side, not only after a server round-trip. Co-Authored-By: Claude Opus 4.7 --- packages/api/openapi.json | 4 ---- packages/api/src/routers/external-api/v2/sources.ts | 4 ---- packages/app/src/components/Sources/SourceForm.tsx | 1 + 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/api/openapi.json b/packages/api/openapi.json index 169529c45e..a2b81197ce 100644 --- a/packages/api/openapi.json +++ b/packages/api/openapi.json @@ -2955,7 +2955,6 @@ "type": "string", "maxLength": 256, "description": "Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together.", - "nullable": true, "example": "Billing" }, "kind": { @@ -3150,7 +3149,6 @@ "type": "string", "maxLength": 256, "description": "Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together.", - "nullable": true, "example": "Billing" }, "kind": { @@ -3372,7 +3370,6 @@ "type": "string", "maxLength": 256, "description": "Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together.", - "nullable": true, "example": "Billing" }, "kind": { @@ -3447,7 +3444,6 @@ "type": "string", "maxLength": 256, "description": "Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together.", - "nullable": true, "example": "Billing" }, "kind": { diff --git a/packages/api/src/routers/external-api/v2/sources.ts b/packages/api/src/routers/external-api/v2/sources.ts index 8cf022100f..106c9a4c76 100644 --- a/packages/api/src/routers/external-api/v2/sources.ts +++ b/packages/api/src/routers/external-api/v2/sources.ts @@ -278,7 +278,6 @@ function formatExternalSource(source: SourceDocument) { * type: string * maxLength: 256 * description: Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together. - * nullable: true * example: Billing * kind: * type: string @@ -431,7 +430,6 @@ function formatExternalSource(source: SourceDocument) { * type: string * maxLength: 256 * description: Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together. - * nullable: true * example: Billing * kind: * type: string @@ -605,7 +603,6 @@ function formatExternalSource(source: SourceDocument) { * type: string * maxLength: 256 * description: Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together. - * nullable: true * example: Billing * kind: * type: string @@ -663,7 +660,6 @@ function formatExternalSource(source: SourceDocument) { * type: string * maxLength: 256 * description: Optional grouping label used to organize sources in the source selector. Sources that share a section value are displayed together. - * nullable: true * example: Billing * kind: * type: string diff --git a/packages/app/src/components/Sources/SourceForm.tsx b/packages/app/src/components/Sources/SourceForm.tsx index e81facf11d..308b81fd6a 100644 --- a/packages/app/src/components/Sources/SourceForm.tsx +++ b/packages/app/src/components/Sources/SourceForm.tsx @@ -2452,6 +2452,7 @@ export function TableSourceForm({ control={control} name="section" placeholder="Optional group, e.g. Billing or Control Plane Prod" + maxLength={256} /> From 9446407ae4c66d693f0ac16833b66d98f6ef110e Mon Sep 17 00:00:00 2001 From: Alex Fedotyev <61838744+alex-fedotyev@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:11:09 +0000 Subject: [PATCH 3/3] chore(sources): remove implementation-context comment on section field Karl noted the comment on `section` describes implementation context rather than wider functionality. Drop it; the surrounding fields in BaseSourceSchema carry no comments and the field is self-explanatory. Co-Authored-By: Claude Opus 4.7 --- packages/common-utils/src/types.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/common-utils/src/types.ts b/packages/common-utils/src/types.ts index 83f6e790e0..92062970ba 100644 --- a/packages/common-utils/src/types.ts +++ b/packages/common-utils/src/types.ts @@ -1582,11 +1582,6 @@ const RequiredTimestampColumnSchema = z export const BaseSourceSchema = z.object({ id: z.string(), name: z.string().min(1, 'Name is required'), - // Optional, free-text grouping label for a source. Persisted and exposed - // through the read API; the source selector will group and search by it in - // a later change. The cap lives on the shared schema so every writer (form, - // internal API, future migrations) inherits it; sources without a section - // keep today's behavior. section: z.string().max(256).optional(), kind: z.nativeEnum(SourceKind), connection: z.string().min(1, 'Server Connection is required'),