Files
insights/products/metrics/frontend/components/metricsStarterDashboardLogic.tsx
zeekay 40d120dbf7 refactor(api): say /v1/ in the Python callers
Every first-party Python caller and test now names the mount the API actually
answers on. Mechanical: URL literals only, and only where the /api/ was ours --
a preceding word character means it was a module path like insights/api/ or a
third-party host like slack.com/api/, and those are untouched.

Left saying /api/, each for a reason rather than for safety:
- products/warehouse_sources/.../sources/** is a catalogue of vendor endpoints
- services/llm-gateway deploys separately and is under a code freeze, so moving
  its two calls would break it during the skew
- the mcp_store and Slack/Teams redirect_uris are registered with third parties
  and must byte-match what was registered
- the Wizard and Raycast client-metadata paths are OAuth client_ids
- /api/surveys and /api/feature_flag/local_evaluation are SDK aliases the
  ingress routes to the Rust fleets, held by released SDK versions

Test URLconfs moved with the requests that call them, and the Datastore
user-selection fixtures now carry the leading slash that request.path has.
2026-08-08 18:38:44 -07:00

271 lines
11 KiB
TypeScript

import { MakeLogicType, actions, connect, kea, listeners, path, reducers } from 'kea'
import { loaders } from 'kea-loaders'
import { router } from 'kea-router'
import api from 'lib/api'
import { toast } from 'lib/elements/Toast/Toast'
import { insightsApi } from 'scenes/insights/utils/api'
import { teamLogic } from 'scenes/teamLogic'
import { urls } from 'scenes/urls'
import { NodeKind } from '~/queries/schema/schema-general'
import type { MetricsQuery, MetricsQueryClause } from '~/queries/schema/schema-general'
import type { DashboardType } from '~/types'
import { metricsAttributeValuesRetrieve, metricsValuesRetrieve } from 'products/metrics/frontend/generated/api'
import type { _MetricNameApi } from 'products/metrics/frontend/generated/api.schemas'
import type { InsightModel } from '../../../../frontend/src/types'
import { RECOMMENDED_AGGREGATION_BY_TYPE, toKnownMetricType } from './metricsViewerLogic'
// A metric name can appear under more than one OTel type, so option identity
// must include both. \0 can't occur in ingest strings, so the key is collision-free.
export const metricOptionKey = (name: string, metricType: string): string => `${name}\0${metricType}`
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface metricsStarterDashboardLogicValues {
currentTeamId: number | null // teamLogic
creating: boolean
dashboardName: string
isModalOpen: boolean
metricOptions: _MetricNameApi[]
metricOptionsLoading: boolean
selectedMetrics: string[]
serviceName: string
services: string[]
servicesLoading: boolean
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface metricsStarterDashboardLogicActions {
closeModal: () => {
value: true
}
createDashboard: () => {
value: true
}
createDashboardFailure: (message: string) => {
message: string
}
createDashboardSuccess: (
createdDashboard: DashboardType,
warning?: string
) => {
createdDashboard: DashboardType<InsightModel>
warning: string | undefined
}
loadMetricOptions: (_: any) => any
loadMetricOptionsFailure: (
error: string,
errorObject?: any
) => {
error: string
errorObject?: any
}
loadMetricOptionsSuccess: (
metricOptions: _MetricNameApi[],
payload?: any
) => {
metricOptions: _MetricNameApi[]
payload?: any
}
loadServices: (_: any) => any
loadServicesFailure: (
error: string,
errorObject?: any
) => {
error: string
errorObject?: any
}
loadServicesSuccess: (
services: string[],
payload?: any
) => {
services: string[]
payload?: any
}
openModal: () => {
value: true
}
setDashboardName: (dashboardName: string) => {
dashboardName: string
}
setSelectedMetrics: (selectedMetrics: string[]) => {
selectedMetrics: string[]
}
setServiceName: (serviceName: string) => {
serviceName: string
}
}
export type metricsStarterDashboardLogicType = MakeLogicType<
metricsStarterDashboardLogicValues,
metricsStarterDashboardLogicActions
>
// One dashboard, one insight per picked metric, each charted with the
// aggregation its type recommends and scoped to the picked service — the
// onboarding doc's starter set, productized.
export const metricsStarterDashboardLogic = kea<metricsStarterDashboardLogicType>([
path(['products', 'metrics', 'frontend', 'components', 'metricsStarterDashboardLogic']),
connect(() => ({
values: [teamLogic, ['currentTeamId']],
})),
actions({
openModal: true,
closeModal: true,
setDashboardName: (dashboardName: string) => ({ dashboardName }),
setServiceName: (serviceName: string) => ({ serviceName }),
// selectedMetrics holds metricOptionKey() composites, not bare names.
setSelectedMetrics: (selectedMetrics: string[]) => ({ selectedMetrics }),
createDashboard: true,
createDashboardSuccess: (createdDashboard: DashboardType, warning?: string) => ({ createdDashboard, warning }),
createDashboardFailure: (message: string) => ({ message }),
}),
reducers({
isModalOpen: [false, { openModal: () => true, closeModal: () => false }],
// The whole form resets on close so a reopen can't silently mint a near-duplicate.
dashboardName: [
'' as string,
{ setDashboardName: (_, { dashboardName }) => dashboardName, closeModal: () => '' },
],
// '' = all services: the insights get no service filter.
serviceName: ['' as string, { setServiceName: (_, { serviceName }) => serviceName, closeModal: () => '' }],
selectedMetrics: [
[] as string[],
{ setSelectedMetrics: (_, { selectedMetrics }) => selectedMetrics, closeModal: () => [] },
],
// Drives the create button's loading/disabled state so a double click can't
// fire twice; the listener's cache flag is the authoritative in-flight guard.
creating: [
false,
{
createDashboard: () => true,
createDashboardSuccess: () => false,
createDashboardFailure: () => false,
},
],
}),
loaders(({ values }) => ({
services: [
[] as string[],
{
loadServices: async (_, breakpoint) => {
await breakpoint(100)
const response = await metricsAttributeValuesRetrieve(String(values.currentTeamId), {
key: 'service.name',
})
breakpoint()
return response.results.map((value) => value.name)
},
},
],
metricOptions: [
[] as _MetricNameApi[],
{
loadMetricOptions: async (_, breakpoint) => {
await breakpoint(100)
const response = await metricsValuesRetrieve(String(values.currentTeamId), {})
breakpoint()
return response.results
},
},
],
})),
listeners(({ actions, values, cache }) => ({
openModal: () => {
actions.loadServices({})
actions.loadMetricOptions({})
},
createDashboard: async () => {
// kea-loaders doesn't dedupe concurrent runs, so the create is a plain
// listener with an explicit in-flight guard.
if (cache.creatingDashboard) {
return
}
cache.creatingDashboard = true
// Snapshot the form before the first await: a modal dismissed or edited
// mid-create must not change (or empty) what gets created.
const name = values.dashboardName.trim()
const selectedMetrics = [...values.selectedMetrics]
const serviceName = values.serviceName
const optionByKey: Record<string, _MetricNameApi> = Object.fromEntries(
values.metricOptions.map((option) => [metricOptionKey(option.name, option.metric_type), option])
)
// Resolve keys to options up front so a name shared by two OTel types
// keeps the type the user actually picked.
const pickedMetrics: _MetricNameApi[] = selectedMetrics.map(
(key) => optionByKey[key] ?? { name: key, metric_type: '' }
)
let dashboard: DashboardType | null = null
let created = 0
try {
if (!name || !selectedMetrics.length) {
actions.createDashboardFailure('Pick a dashboard name and at least one metric')
return
}
// The core dashboards create endpoint has no generated client yet
// (only its sharing sub-resources are in the codegen surface), so this
// stays a manual call until the Dashboard viewset is tagged.
// nosemgrep: prefer-codegen-api
dashboard = await api.create<DashboardType>(`v1/projects/${values.currentTeamId}/dashboards/`, {
name,
})
for (const { name: metricName, metric_type: rawType } of pickedMetrics) {
// The names endpoint reports raw ingest strings; only enum members
// may reach the API — mirrors the viewer's metricsQueryNode.
const metricType = toKnownMetricType(rawType)
const recommended = (rawType && RECOMMENDED_AGGREGATION_BY_TYPE[rawType]) || 'avg'
const clause: MetricsQueryClause = {
name: 'a',
metricName,
// The REST viewer's 'p95' shorthand maps to the node's quantile aggregation.
aggregation: recommended === 'p95' ? 'quantile' : recommended,
...(recommended === 'p95' ? { quantile: 0.95 } : {}),
...(metricType ? { metricType } : {}),
...(serviceName
? { filters: [{ key: 'service.name', op: 'eq' as const, value: serviceName }] }
: {}),
}
const query: MetricsQuery = { kind: NodeKind.MetricsQuery, clauses: [clause] }
await insightsApi.create({
name: `${metricName} (${recommended})`,
saved: true,
dashboards: [dashboard.id],
query,
})
created++
}
actions.createDashboardSuccess(dashboard)
} catch (error: any) {
if (dashboard) {
// The dashboard exists with a partial insight set — say so and take
// the user there rather than implying nothing happened (a retry
// from the modal would mint a duplicate dashboard). The warning
// replaces the success toast so only one shows.
actions.createDashboardSuccess(
dashboard,
`Dashboard "${dashboard.name}" created, but only ${created} of ${selectedMetrics.length} insights could be added`
)
} else {
actions.createDashboardFailure(error?.detail || error?.message || 'Failed to create the dashboard')
}
} finally {
cache.creatingDashboard = false
}
},
createDashboardSuccess: ({ createdDashboard, warning }) => {
actions.closeModal()
if (warning) {
toast.warning(warning)
} else {
toast.success(`Dashboard "${createdDashboard.name}" created`)
}
router.actions.push(urls.dashboard(createdDashboard.id))
},
createDashboardFailure: ({ message }) => {
toast.error(message)
},
})),
])