Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/analytics/analytics-utilities/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"devDependencies": {
"@kong-ui-public/i18n": "workspace:^",
"@kong/design-tokens": "1.20.0",
"ajv": "^8.17.1",
"json-schema-to-ts": "^3.1.1",
"vue": "^3.5.27"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import Ajv from 'ajv'
import { describe, expect, it } from 'vitest'
import {
apiUsageQuerySchema,
barChartSchema,
basicQuerySchema,
dashboardConfigSchema,
llmUsageSchema,
mcpUsageSchema,
validDashboardQuery,
platformQuerySchema,
} from './dashboardSchema.v2'
import {
aiExploreAggregations,
basicExploreAggregations,
exploreAggregations,
exploreFilterTypesV2,
filterableAiExploreDimensions,
filterableBasicExploreDimensions,
filterableExploreDimensions,
filterableMcpExploreDimensions,
mcpExploreAggregations,
queryableAiExploreDimensions,
queryableBasicExploreDimensions,
queryableExploreDimensions,
queryableMcpExploreDimensions,
requestFilterTypeEmptyV2,
} from './types'

const ajv = new Ajv({ allowUnionTypes: true })
const validateValidDashboardQuery = ajv.compile(validDashboardQuery)
const validatePlatformQuerySchema = ajv.compile(platformQuerySchema)
const validateDashboardConfigSchema = ajv.compile(dashboardConfigSchema)
const validateApiUsageQuerySchema = ajv.compile(apiUsageQuerySchema)
const validateBasicQuerySchema = ajv.compile(basicQuerySchema)
const validateLlmUsageQuerySchema = ajv.compile(llmUsageSchema)
const validateMcpUsageQuerySchema = ajv.compile(mcpUsageSchema)

describe('dashboardSchema.v2', () => {
const sharedPresetFilterableDimensions = [
...new Set([
...filterableExploreDimensions,
...filterableBasicExploreDimensions,
...filterableAiExploreDimensions,
...filterableMcpExploreDimensions,
]),
]

const platformQuery = {
datasource: 'platform',
metrics: ['custom_metric_name'],
dimensions: ['custom_dimension_name'],
filters: [
{
operator: 'custom_platform_operator',
field: 'custom_filter_field',
value: ['custom_value'],
},
],
}

const dashboardConfig = {
tiles: [
{
type: 'chart',
definition: {
query: {
...platformQuery,
},
chart: {
type: 'horizontal_bar',
},
},
layout: {
position: {
col: 1,
row: 1,
},
size: {
cols: 1,
rows: 1,
},
},
},
],
preset_filters: [
{
operator: 'in',
field: 'gateway_service',
value: ['example-service'],
},
],
}

const mixedDashboardConfig = {
...dashboardConfig,
tiles: [
dashboardConfig.tiles[0],
{
type: 'chart',
definition: {
query: {
datasource: 'api_usage',
metrics: ['request_count'],
dimensions: ['gateway_service'],
filters: [
{
operator: 'in',
field: 'gateway_service',
value: ['example-service'],
},
],
},
chart: {
type: 'horizontal_bar',
},
},
layout: {
position: {
col: 2,
row: 1,
},
size: {
cols: 1,
rows: 1,
},
},
},
],
}

const strictQuery = {
datasource: 'api_usage',
metrics: ['request_count'],
dimensions: ['gateway_service'],
filters: [
{
operator: 'in',
field: 'gateway_service',
value: ['example-service'],
},
],
}

it('accepts platform queries with arbitrary strings at runtime', () => {
expect(validatePlatformQuerySchema(platformQuery)).toBe(true)
expect(validateValidDashboardQuery(platformQuery)).toBe(true)
expect(validateDashboardConfigSchema(dashboardConfig)).toBe(true)
expect(validateDashboardConfigSchema(mixedDashboardConfig)).toBe(true)
})

it.each([
[apiUsageQuerySchema, exploreAggregations, queryableExploreDimensions, filterableExploreDimensions],
[basicQuerySchema, basicExploreAggregations, queryableBasicExploreDimensions, filterableBasicExploreDimensions],
[llmUsageSchema, aiExploreAggregations, queryableAiExploreDimensions, filterableAiExploreDimensions],
[mcpUsageSchema, mcpExploreAggregations, queryableMcpExploreDimensions, filterableMcpExploreDimensions],
])('keeps strict enums for existing datasource schemas', (schema, expectedMetrics, expectedDimensions, expectedFilterableDimensions) => {
expect(schema.properties.datasource.enum).toHaveLength(1)
expect(schema.properties.metrics.items.enum).toEqual(expectedMetrics)
expect(schema.properties.dimensions.items.enum).toEqual(expectedDimensions)
expect(schema.properties.filters.items.oneOf[0].properties.field.enum).toEqual(expectedFilterableDimensions)
expect(schema.properties.filters.items.oneOf[1].properties.field.enum).toEqual(expectedFilterableDimensions)
})

it('loosens only the platform branch', () => {
expect(platformQuerySchema.properties.datasource.enum).toEqual(['platform'])
expect(platformQuerySchema.properties.metrics.items.enum).toBeUndefined()
expect(platformQuerySchema.properties.dimensions.items.enum).toBeUndefined()
expect(platformQuerySchema.properties.filters.items.oneOf[0].properties.field.enum).toBeUndefined()
expect(platformQuerySchema.properties.filters.items.oneOf[1].properties.field.enum).toBeUndefined()
expect(platformQuerySchema.properties.filters.items.oneOf[0].properties.operator.enum).toBeUndefined()
expect(platformQuerySchema.properties.filters.items.oneOf[1].properties.operator.enum).toBeUndefined()
})

it('keeps shared preset filters strict and operator enums intact', () => {
expect(dashboardConfigSchema.properties.preset_filters.items.oneOf[0].properties.field.enum).toEqual(sharedPresetFilterableDimensions)
expect(dashboardConfigSchema.properties.preset_filters.items.oneOf[1].properties.field.enum).toEqual(sharedPresetFilterableDimensions)
expect(dashboardConfigSchema.properties.preset_filters.items.oneOf[0].properties.operator.enum).toEqual(exploreFilterTypesV2)
expect(dashboardConfigSchema.properties.preset_filters.items.oneOf[1].properties.operator.enum).toEqual(requestFilterTypeEmptyV2)
expect(barChartSchema.properties.type.enum).toEqual(['horizontal_bar', 'vertical_bar'])
})

it('rejects arbitrary strings for strict schemas', () => {
const invalidStrictQuery = {
...strictQuery,
metrics: ['custom_metric_name'],
dimensions: ['custom_dimension_name'],
filters: [
{
operator: 'in',
field: 'custom_filter_field',
value: ['custom_value'],
},
],
}

expect(validateApiUsageQuerySchema(invalidStrictQuery)).toBe(false)
expect(validateBasicQuerySchema({
...invalidStrictQuery,
datasource: 'basic',
})).toBe(false)
expect(validateLlmUsageQuerySchema({
...invalidStrictQuery,
datasource: 'llm_usage',
})).toBe(false)
expect(validateMcpUsageQuerySchema({
...invalidStrictQuery,
datasource: 'agentic_usage',
})).toBe(false)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -369,27 +369,27 @@ const baseQueryProperties = {
},
} as const

const metricsFn = <T extends readonly string[]>(aggregations: T) => ({
const metricsFn = <T extends readonly string[] | undefined>(aggregations?: T) => ({
type: 'array',
description: 'List of aggregated metrics to collect across the requested time span.',
items: {
type: 'string',
enum: aggregations,
...(aggregations ? { enum: aggregations } : {}),
},
} as const satisfies JSONSchema)

const dimensionsFn = <T extends readonly string[]>(dimensions: T) => ({
const dimensionsFn = <T extends readonly string[] | undefined>(dimensions?: T) => ({
type: 'array',
description: 'List of attributes or entity types to group by.',
minItems: 0,
maxItems: 2,
items: {
type: 'string',
enum: dimensions,
...(dimensions ? { enum: dimensions } : {}),
},
} as const satisfies JSONSchema)

const filtersFn = <T extends readonly string[]>(filterableDimensions: T) => ({
const filtersFn = <T extends readonly string[] | undefined>(filterableDimensions?: T) => ({
type: 'array',
description: 'A list of filters to apply to the query',
items: {
Expand All @@ -400,7 +400,7 @@ const filtersFn = <T extends readonly string[]>(filterableDimensions: T) => ({
properties: {
field: {
type: 'string',
enum: filterableDimensions,
...(filterableDimensions ? { enum: filterableDimensions } : {}),
},
operator: {
type: 'string',
Expand All @@ -426,7 +426,7 @@ const filtersFn = <T extends readonly string[]>(filterableDimensions: T) => ({
properties: {
field: {
type: 'string',
enum: filterableDimensions,
...(filterableDimensions ? { enum: filterableDimensions } : {}),
},
operator: {
type: 'string',
Expand All @@ -443,6 +443,56 @@ const filtersFn = <T extends readonly string[]>(filterableDimensions: T) => ({
},
} as const satisfies JSONSchema)

const platformFiltersFn = () => ({
type: 'array',
description: 'A list of filters to apply to the platform query',
items: {
oneOf: [
{
type: 'object',
description: 'In filter',
properties: {
field: {
type: 'string',
},
operator: {
type: 'string',
},
value: {
type: 'array',
items: {
type: ['string', 'number', 'null'],
},
},
},
required: [
'field',
'operator',
'value',
],
additionalProperties: false,
},
{
type: 'object',
description: 'Empty filter',
properties: {
field: {
type: 'string',
},
operator: {
type: 'string',
},
},
required: [
'field',
'operator',
],
additionalProperties: false,
},
],
},
} as const satisfies JSONSchema)

export const apiUsageQuerySchema = {
type: 'object',
description: 'A query to launch at the advanced explore API',
Expand Down Expand Up @@ -519,8 +569,27 @@ export const mcpUsageSchema = {
additionalProperties: false,
} as const satisfies JSONSchema

export const platformQuerySchema = {
type: 'object',
description: 'A query to launch at the platform dashboard API',
properties: {
datasource: {
type: 'string',
enum: [
'platform',
],
},
metrics: metricsFn(),
dimensions: dimensionsFn(),
filters: platformFiltersFn(),
...baseQueryProperties,
},
required: ['datasource'],
additionalProperties: false,
} as const satisfies JSONSchema

export const validDashboardQuery = {
anyOf: [apiUsageQuerySchema, basicQuerySchema, llmUsageSchema, mcpUsageSchema],
anyOf: [apiUsageQuerySchema, basicQuerySchema, llmUsageSchema, mcpUsageSchema, platformQuerySchema],
} as const satisfies JSONSchema

export type ValidDashboardQuery = FromSchemaWithOptions<typeof validDashboardQuery>
Expand Down
7 changes: 5 additions & 2 deletions packages/analytics/analytics-utilities/src/filters.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AllFilterableDimensionsAndMetrics, FilterDatasource } from './types'
import type { FilterDatasource } from './types'
import {
filterableAiExploreDimensions,
filterableBasicExploreDimensions,
Expand All @@ -9,8 +9,11 @@ import {
} from './types'


/**
* @deprecated Use `useDatasourceConfigStore().getFieldDataSources` from `@kong-ui-public/analytics-config-store`.
*/
export const getFieldDataSources = (
dimension: AllFilterableDimensionsAndMetrics,
dimension: string,
): FilterDatasource[] => {
const datasources: FilterDatasource[] = []

Expand Down
Loading
Loading