mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
266 lines
12 KiB
TypeScript
266 lines
12 KiB
TypeScript
import { MakeLogicType, actions, connect, kea, listeners, path } from 'kea'
|
|
import insights from 'insights-js'
|
|
|
|
import { teamLogic } from 'scenes/teamLogic'
|
|
|
|
import { type MetricsQuery, ProductIntentContext, ProductKey } from '~/queries/schema/schema-general'
|
|
|
|
import type { _MetricEventSampleApi } from 'products/metrics/frontend/generated/api.schemas'
|
|
|
|
import type { MetricSummary } from '../../../../frontend/src/lib/components/Metric/metricSummary'
|
|
import type { ProductIntentProperties } from '../../../../frontend/src/lib/utils/product-intents'
|
|
import type { Node } from '../../../../frontend/src/queries/schema/schema-general'
|
|
import type { QueryBasedInsightModel, UniversalFiltersGroup } from '../../../../frontend/src/types'
|
|
import type { _MetricFilterApi, _MetricSeriesApi } from '../generated/api.schemas'
|
|
import { metricsSceneLogic } from '../metricsSceneLogic'
|
|
import type { MetricsSceneActiveTab } from '../metricsSceneLogic'
|
|
import { metricsSamplesLogic } from './metricsSamplesLogic'
|
|
import type { MetricsPanelTab } from './metricsSamplesLogic'
|
|
import { isUserInitiatedError, metricsViewerLogic } from './metricsViewerLogic'
|
|
import type { MetricAggregation, MetricsViewMode } from './metricsViewerLogic'
|
|
|
|
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
|
|
export interface metricsUsageTrackingLogicValues {
|
|
aggregation: MetricAggregation // metricsViewerLogic
|
|
groupByKeys: string[] // metricsViewerLogic
|
|
hasMetricName: boolean // metricsViewerLogic
|
|
queryFilters: _MetricFilterApi[] // metricsViewerLogic
|
|
}
|
|
|
|
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
|
|
export interface metricsUsageTrackingLogicActions {
|
|
loadSamplesSuccess: (
|
|
samples: _MetricEventSampleApi[],
|
|
payload?: any
|
|
) => {
|
|
payload?: any
|
|
samples: _MetricEventSampleApi[]
|
|
} // metricsSamplesLogic
|
|
samplesPanelTabChanged: (activeTab: MetricsPanelTab) => {
|
|
activeTab: MetricsPanelTab
|
|
} // metricsSamplesLogic
|
|
sceneTabChanged: (activeTab: MetricsSceneActiveTab) => {
|
|
activeTab: MetricsSceneActiveTab
|
|
} // metricsSceneLogic
|
|
addToDashboard: () => {
|
|
value: true
|
|
} // metricsViewerLogic
|
|
fetchQueryResults: (_: any) => any // metricsViewerLogic
|
|
fetchQueryResultsFailure: (
|
|
error: string,
|
|
errorObject?: any
|
|
) => {
|
|
error: string
|
|
errorObject?: any
|
|
} // metricsViewerLogic
|
|
fetchQueryResultsSuccess: (
|
|
queryResults: _MetricSeriesApi[],
|
|
payload?: any
|
|
) => {
|
|
payload?: any
|
|
queryResults: _MetricSeriesApi[]
|
|
} // metricsViewerLogic
|
|
saveAsInsightSuccess: (
|
|
savedInsight: QueryBasedInsightModel<Node<Record<string, any>>> | null,
|
|
payload?: any
|
|
) => {
|
|
payload?: any
|
|
savedInsight: QueryBasedInsightModel<Node<Record<string, any>>> | null
|
|
} // metricsViewerLogic
|
|
setAggregation: (aggregation: MetricAggregation) => {
|
|
aggregation: MetricAggregation
|
|
} // metricsViewerLogic
|
|
setDateFrom: (dateFrom: string | null) => {
|
|
dateFrom: string | null
|
|
} // metricsViewerLogic
|
|
setFilterGroup: (filterGroup: UniversalFiltersGroup) => {
|
|
filterGroup: UniversalFiltersGroup
|
|
} // metricsViewerLogic
|
|
setGroupByKeys: (groupByKeys: string[]) => {
|
|
groupByKeys: string[]
|
|
} // metricsViewerLogic
|
|
setLiveRefresh: (liveRefresh: boolean) => {
|
|
liveRefresh: boolean
|
|
} // metricsViewerLogic
|
|
setMetricName: (metricName: string) => {
|
|
metricName: string
|
|
} // metricsViewerLogic
|
|
setStatSummary: (statSummary: MetricSummary) => {
|
|
statSummary: MetricSummary
|
|
} // metricsViewerLogic
|
|
setViewMode: (viewMode: MetricsViewMode) => {
|
|
viewMode: MetricsViewMode
|
|
} // metricsViewerLogic
|
|
addProductIntent: (properties: ProductIntentProperties) => ProductIntentProperties // teamLogic
|
|
exemplarDotClicked: (hasSpanId: boolean) => {
|
|
hasSpanId: boolean
|
|
}
|
|
sampleRowExpanded: (sample: _MetricEventSampleApi) => {
|
|
sample: _MetricEventSampleApi
|
|
}
|
|
tracePivotClicked: (sample: _MetricEventSampleApi) => {
|
|
sample: _MetricEventSampleApi
|
|
}
|
|
}
|
|
|
|
export type metricsUsageTrackingLogicType = MakeLogicType<
|
|
metricsUsageTrackingLogicValues,
|
|
metricsUsageTrackingLogicActions
|
|
>
|
|
|
|
// Central usage tracking for the metrics scene, following the metricsSqlEditorTrackingLogic
|
|
// precedent: captures fire from listeners on the source logics' actions, not from onClick
|
|
// handlers scattered through components.
|
|
//
|
|
// Privacy rule: never capture customer metric names, series labels, attribute keys, or
|
|
// attribute values — only counts, booleans, durations, and our own enum values.
|
|
export const metricsUsageTrackingLogic = kea<metricsUsageTrackingLogicType>([
|
|
path(['products', 'metrics', 'frontend', 'components', 'metricsUsageTrackingLogic']),
|
|
connect(() => ({
|
|
actions: [
|
|
metricsSceneLogic,
|
|
['setActiveTab as sceneTabChanged'],
|
|
metricsViewerLogic,
|
|
[
|
|
'setMetricName',
|
|
'setAggregation',
|
|
'setDateFrom',
|
|
'setViewMode',
|
|
'setStatSummary',
|
|
'setLiveRefresh',
|
|
'setGroupByKeys',
|
|
'setFilterGroup',
|
|
'addToDashboard',
|
|
'saveAsInsightSuccess',
|
|
'fetchQueryResults',
|
|
'fetchQueryResultsSuccess',
|
|
'fetchQueryResultsFailure',
|
|
],
|
|
metricsSamplesLogic,
|
|
['setActiveTab as samplesPanelTabChanged', 'loadSamplesSuccess'],
|
|
teamLogic,
|
|
['addProductIntent'],
|
|
],
|
|
values: [metricsViewerLogic, ['hasMetricName', 'aggregation', 'groupByKeys', 'queryFilters']],
|
|
})),
|
|
actions({
|
|
// Component-only interactions with no source-logic action to listen to.
|
|
sampleRowExpanded: (sample: _MetricEventSampleApi) => ({ sample }),
|
|
tracePivotClicked: (sample: _MetricEventSampleApi) => ({ sample }),
|
|
exemplarDotClicked: (hasSpanId: boolean) => ({ hasSpanId }),
|
|
}),
|
|
listeners(({ actions, values, cache }) => ({
|
|
sceneTabChanged: ({ activeTab }) => {
|
|
insights.capture('metrics tab changed', { tab: activeTab })
|
|
},
|
|
setMetricName: ({ metricName }) => {
|
|
if (metricName.trim()) {
|
|
insights.capture('metrics viewer metric selected')
|
|
}
|
|
},
|
|
setAggregation: ({ aggregation }) => {
|
|
insights.capture('metrics viewer aggregation changed', { aggregation })
|
|
},
|
|
// The DateFilter dispatches setDateFrom and setDateTo together on every change,
|
|
// so listening to setDateFrom alone captures each range change exactly once.
|
|
setDateFrom: ({ dateFrom }) => {
|
|
insights.capture('metrics viewer date range changed', { date_from: dateFrom })
|
|
},
|
|
setViewMode: ({ viewMode }) => {
|
|
insights.capture('metrics viewer view mode changed', { view_mode: viewMode })
|
|
},
|
|
setStatSummary: ({ statSummary }) => {
|
|
insights.capture('metrics viewer stat summary changed', { stat_summary: statSummary })
|
|
},
|
|
setLiveRefresh: ({ liveRefresh }) => {
|
|
insights.capture('metrics viewer live toggled', { enabled: liveRefresh })
|
|
},
|
|
setGroupByKeys: ({ groupByKeys }) => {
|
|
insights.capture('metrics viewer group by changed', { group_by_count: groupByKeys.length })
|
|
},
|
|
setFilterGroup: () => {
|
|
// queryFilters counts only complete, backend-valid chips (reducers ran before us).
|
|
insights.capture('metrics viewer attribute filter changed', { filter_count: values.queryFilters.length })
|
|
},
|
|
addToDashboard: () => {
|
|
insights.capture('metrics add to dashboard clicked', { aggregation: values.aggregation })
|
|
},
|
|
saveAsInsightSuccess: ({ savedInsight }) => {
|
|
if (!savedInsight) {
|
|
return
|
|
}
|
|
// Read the aggregation off the insight itself — the viewer's current
|
|
// value can already differ if it changed while the save was in flight.
|
|
// The node's 'quantile' maps back to the viewer vocabulary's 'p95'.
|
|
const nodeAggregation = (savedInsight.query as MetricsQuery | undefined)?.clauses?.[0]?.aggregation
|
|
insights.capture('metrics insight saved', {
|
|
aggregation: nodeAggregation === 'quantile' ? 'p95' : (nodeAggregation ?? null),
|
|
})
|
|
},
|
|
fetchQueryResults: () => {
|
|
cache.queryStartedAt = performance.now()
|
|
},
|
|
fetchQueryResultsSuccess: ({ queryResults }) => {
|
|
// The loader resolves [] without querying when no metric is picked yet — not a query.
|
|
if (!values.hasMetricName) {
|
|
return
|
|
}
|
|
// load_ms includes the loader's 300ms debounce breakpoint — consistent overhead,
|
|
// fine for trend comparisons.
|
|
const loadMs =
|
|
cache.queryStartedAt !== undefined ? Math.round(performance.now() - cache.queryStartedAt) : null
|
|
insights.capture('metrics query completed', {
|
|
series_count: queryResults.length,
|
|
point_count: queryResults.reduce((sum: number, series) => sum + series.points.length, 0),
|
|
load_ms: loadMs,
|
|
aggregation: values.aggregation,
|
|
has_group_by: values.groupByKeys.length > 0,
|
|
has_filters: values.queryFilters.length > 0,
|
|
})
|
|
// Once per mount, not per query — live refresh re-runs the query every 15s,
|
|
// and each intent call is an API request (the backend counts repeats itself).
|
|
if (!cache.viewerQueryIntentFired) {
|
|
cache.viewerQueryIntentFired = true
|
|
actions.addProductIntent({
|
|
product_type: ProductKey.METRICS,
|
|
intent_context: ProductIntentContext.METRICS_VIEWER_QUERY_RUN,
|
|
})
|
|
}
|
|
},
|
|
fetchQueryResultsFailure: ({ error, errorObject }) => {
|
|
// A superseded/unmounted query aborts — user-initiated, not a failure.
|
|
if (isUserInitiatedError(error) || isUserInitiatedError(errorObject)) {
|
|
return
|
|
}
|
|
// Only a coarse class — the message can embed the customer's metric name.
|
|
const status = (errorObject as { status?: number } | undefined)?.status
|
|
insights.capture('metrics query failed', { error_type: status ? `http_${status}` : 'unknown' })
|
|
},
|
|
samplesPanelTabChanged: ({ activeTab }) => {
|
|
insights.capture('metrics samples panel tab changed', { tab: activeTab })
|
|
},
|
|
loadSamplesSuccess: ({ samples }) => {
|
|
if (!values.hasMetricName) {
|
|
return
|
|
}
|
|
const withTrace = samples.filter((sample) => !!sample.trace_id).length
|
|
insights.capture('metrics samples loaded', {
|
|
sample_count: samples.length,
|
|
trace_coverage: samples.length ? Math.round((100 * withTrace) / samples.length) : 0,
|
|
})
|
|
},
|
|
sampleRowExpanded: ({ sample }) => {
|
|
insights.capture('metrics sample row expanded', {
|
|
attribute_count: Object.keys(sample.attributes).length + Object.keys(sample.resource_attributes).length,
|
|
has_trace: !!sample.trace_id,
|
|
})
|
|
},
|
|
tracePivotClicked: ({ sample }) => {
|
|
insights.capture('metrics trace pivot clicked', { has_span_id: !!sample.span_id })
|
|
},
|
|
exemplarDotClicked: ({ hasSpanId }) => {
|
|
insights.capture('metrics exemplar dot clicked', { has_span_id: hasSpanId })
|
|
},
|
|
})),
|
|
])
|