Files
zeekay 1b367e2b62 chore: rebase the tree onto upstream master
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.
2026-08-01 15:44:40 -07:00

659 lines
27 KiB
TypeScript

import { deepEqual as equal } from 'fast-equals'
import { MakeLogicType, actions, connect, kea, listeners, path, reducers, selectors } from 'kea'
import { router, urlToAction } from 'kea-router'
import { subscriptions } from 'kea-subscriptions'
import { DEFAULT_UNIVERSAL_GROUP_FILTER } from 'lib/components/UniversalFilters/constants'
import { FEATURE_FLAGS } from 'lib/constants'
import { featureFlagLogic, type FeatureFlagsSet } from 'lib/logic/featureFlagLogic'
import { trackedActionToUrl } from 'lib/logic/scenes/trackedActionToUrl'
import { parseTagsFilter } from 'lib/utils/url'
import { Params } from 'scenes/sceneTypes'
import { Breadcrumb } from '~/types'
import type { AggregatedSpanRow, DateRange, SpanTreeNode } from '../../../frontend/src/queries/schema/schema-general'
import type { UniversalFiltersGroup } from '../../../frontend/src/types'
import type { TracingDurationHistogramData, VisibleDurationRange } from './durationBuckets'
import type { TracingLatencyHeatmapData } from './durationBuckets'
import { tracingDataLogic } from './tracingDataLogic'
import type { TracingSparklineData, VisibleSpanTimeRange } from './tracingDataLogic'
import {
DEFAULT_CUSTOM_COMPARISON,
DEFAULT_DATE_RANGE,
DEFAULT_ORDER_BY,
DEFAULT_ORDER_DIRECTION,
DEFAULT_SERVICE_NAMES,
DEFAULT_CHART_TYPE,
DEFAULT_VIEW_MODE,
parseComparison,
serializeComparison,
TRACING_SCENE_VIEWER_ID,
type TracingComparison,
tracingFiltersLogic,
} from './tracingFiltersLogic'
import type {
OverlayWindow,
TimeComparison,
TracingFilters,
TracingOrderBy,
TracingOrderDirection,
TracingViewMode,
} from './tracingFiltersLogic'
import { tracingViewerLogic } from './tracingViewerLogic'
import type { Span } from './types'
/** What each result row represents: a trace (its root span), an individual span, or an operation aggregate. */
export type TracingDisplayMode = TracingViewMode | 'operations'
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface tracingSceneLogicValues {
featureFlags: FeatureFlagsSet // featureFlagLogic
aggregation: {
current: AggregatedSpanRow[]
previous: AggregatedSpanRow[] | null
} // tracingDataLogic
aggregationLoading: boolean // tracingDataLogic
durationHistogramData: TracingDurationHistogramData // tracingDataLogic
durationHistogramLoading: boolean // tracingDataLogic
hasMoreToLoad: boolean // tracingDataLogic
hasRunQuery: boolean // tracingDataLogic
isDurationMode: boolean // tracingDataLogic
latencyHeatmapData: TracingLatencyHeatmapData // tracingDataLogic
latencyHeatmapLoading: boolean // tracingDataLogic
listRows: Span[] // tracingDataLogic
showHeatmap: boolean // tracingDataLogic
spanTree: {
current: SpanTreeNode[]
previous: SpanTreeNode[] | null
spanName: string | null
} // tracingDataLogic
spanTreeLoading: boolean // tracingDataLogic
spans: Span[] // tracingDataLogic
spansLoading: boolean // tracingDataLogic
sparklineData: TracingSparklineData // tracingDataLogic
sparklineLoading: boolean // tracingDataLogic
totalMatchingFilters: number // tracingDataLogic
traceSpans: Span[] // tracingDataLogic
traceSpansHasMore: boolean // tracingDataLogic
traceSpansLoading: boolean // tracingDataLogic
traceSpansLoadingMore: boolean // tracingDataLogic
visibleRowDateRange: VisibleSpanTimeRange | null // tracingDataLogic
visibleRowDurationRange: VisibleDurationRange | null // tracingDataLogic
compareActive: boolean // tracingFiltersLogic
currentWindowMs: OverlayWindow // tracingFiltersLogic
filters: TracingFilters // tracingFiltersLogic
previousWindowMs: OverlayWindow // tracingFiltersLogic
sparklineWindowMs: OverlayWindow // tracingFiltersLogic
utcDateRange: {
date_from: string | null | undefined
date_to: string | null | undefined
} // tracingFiltersLogic
canLoadMoreTraceSpans: boolean // tracingViewerLogic
compareFlameServiceName: string | null // tracingViewerLogic
compareFlameSpanName: string | null // tracingViewerLogic
isLoadingFullTrace: boolean // tracingViewerLogic
isTraceOpen: boolean // tracingViewerLogic
openTraceSpans: Span[] // tracingViewerLogic
selectedSpanId: string | null // tracingViewerLogic
selectedTraceId: string | null // tracingViewerLogic
selectedTraceTs: string | null // tracingViewerLogic
activeTracingTab: 'operations' | 'traces'
breadcrumbs: Breadcrumb[]
displayMode: TracingDisplayMode
operationsViewEnabled: boolean
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface tracingSceneLogicActions {
applyHeatmapBrush: (selection: import('./heatmapBrush').HeatmapBrushSelection) => {
selection: import('./heatmapBrush').HeatmapBrushSelection
} // tracingDataLogic
fetchAggregation: (
options?:
| {
fullRange?: boolean
}
| null
| undefined
) => {
fullRange?: boolean
} | null // tracingDataLogic
fetchNextPage: () => {
value: true
} // tracingDataLogic
fetchSpanTree: (params: { serviceName: string; spanName: string }) => {
serviceName: string
spanName: string
} // tracingDataLogic
handleFilterChange: (
filterType: string,
extraProps?: Record<string, unknown> | undefined
) => {
extraProps: Record<string, unknown> | undefined
filterType: string
} // tracingDataLogic
loadMoreTraceSpans: () => {
value: true
} // tracingDataLogic
loadTraceSpans: (args_0: { traceId: string; ts?: string | null }) => {
traceId: string
ts?: string | null
} // tracingDataLogic
runQuery: () => {
value: true
} // tracingDataLogic
setVisibleRowRange: (
startIndex: number,
stopIndex: number
) => {
startIndex: number
stopIndex: number
} // tracingDataLogic
setChartType: (chartType: import('./tracingFiltersLogic').TracingChartType) => {
chartType: import('./tracingFiltersLogic').TracingChartType
} // tracingFiltersLogic
setComparison: (comparison: TimeComparison | null) => {
comparison: TimeComparison | null
} // tracingFiltersLogic
setDateRange: (dateRange: DateRange) => {
dateRange: DateRange
} // tracingFiltersLogic
setFilterGroup: (filterGroup: UniversalFiltersGroup) => {
filterGroup: UniversalFiltersGroup
} // tracingFiltersLogic
setFilters: (filters: Partial<TracingFilters>) => {
filters: Partial<TracingFilters>
} // tracingFiltersLogic
setServiceNames: (serviceNames: string[]) => {
serviceNames: string[]
} // tracingFiltersLogic
setSort: (
orderBy: TracingOrderBy,
orderDirection: TracingOrderDirection
) => {
orderBy: TracingOrderBy
orderDirection: TracingOrderDirection
} // tracingFiltersLogic
setViewMode: (viewMode: TracingViewMode) => {
viewMode: TracingViewMode
} // tracingFiltersLogic
updateComparisonWindows: (
current: OverlayWindow,
previous: OverlayWindow
) => {
current: OverlayWindow
previous: OverlayWindow
} // tracingFiltersLogic
closeCompareFlame: () => {
value: true
} // tracingViewerLogic
closeTrace: () => {
value: true
} // tracingViewerLogic
openCompareFlame: (
spanName: string,
serviceName: string
) => {
serviceName: string
spanName: string
} // tracingViewerLogic
openTrace: (
traceId: string,
options?:
| {
spanId?: string | null
ts?: string | null
}
| undefined
) => {
spanId: string | null
traceId: string
ts: string | null
} // tracingViewerLogic
selectSpan: (spanId: string | null) => {
spanId: string | null
} // tracingViewerLogic
setActiveTracingTab: (tab: 'operations' | 'traces') => {
tab: 'operations' | 'traces'
}
setDisplayMode: (mode: TracingDisplayMode) => {
mode: TracingDisplayMode
}
syncUrl: () => {
value: true
}
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface tracingSceneLogicMeta {
__keaTypeGenInternalSelectorTypes: {
operationsViewEnabled: (featureFlags: FeatureFlagsSet) => boolean
displayMode: (
operationsViewEnabled: boolean,
activeTracingTab: 'operations' | 'traces',
filters: TracingFilters
) => TracingDisplayMode
}
}
export type tracingSceneLogicType = MakeLogicType<
tracingSceneLogicValues,
tracingSceneLogicActions,
Record<string, any>,
tracingSceneLogicMeta
>
export const tracingSceneLogic = kea<tracingSceneLogicType>([
path(['products', 'tracing', 'frontend', 'tracingSceneLogic']),
// The scene binds to the viewer-default instances of the keyed filter/data/viewer logics.
// It owns ALL URL sync — the keyed logics are URL-free so they can be embedded anywhere;
// this logic translates URL params to their actions and their actions back to URL writes.
// Mirrors the logsSceneLogic / logsViewerLogic boundary.
connect(() => ({
values: [
featureFlagLogic,
['featureFlags'],
tracingDataLogic({ id: TRACING_SCENE_VIEWER_ID }),
[
'spans',
'spansLoading',
'listRows',
'sparklineData',
'sparklineLoading',
'hasMoreToLoad',
'hasRunQuery',
'totalMatchingFilters',
'traceSpans',
'traceSpansLoading',
'traceSpansLoadingMore',
'traceSpansHasMore',
'aggregation',
'aggregationLoading',
'spanTree',
'spanTreeLoading',
'visibleRowDateRange',
'durationHistogramData',
'durationHistogramLoading',
'visibleRowDurationRange',
'isDurationMode',
'latencyHeatmapData',
'latencyHeatmapLoading',
'showHeatmap',
],
tracingFiltersLogic({ id: TRACING_SCENE_VIEWER_ID }),
['filters', 'utcDateRange', 'sparklineWindowMs', 'currentWindowMs', 'previousWindowMs', 'compareActive'],
tracingViewerLogic({ id: TRACING_SCENE_VIEWER_ID }),
[
'selectedTraceId',
'selectedSpanId',
'selectedTraceTs',
'isTraceOpen',
'openTraceSpans',
'isLoadingFullTrace',
'canLoadMoreTraceSpans',
'compareFlameSpanName',
'compareFlameServiceName',
],
],
actions: [
tracingDataLogic({ id: TRACING_SCENE_VIEWER_ID }),
[
'runQuery',
'fetchNextPage',
'loadTraceSpans',
'loadMoreTraceSpans',
'fetchAggregation',
'fetchSpanTree',
'setVisibleRowRange',
'handleFilterChange',
'applyHeatmapBrush',
],
tracingFiltersLogic({ id: TRACING_SCENE_VIEWER_ID }),
[
'setDateRange',
'setServiceNames',
'setFilterGroup',
'setSort',
'setViewMode',
'setChartType',
'setComparison',
'updateComparisonWindows',
'setFilters',
],
tracingViewerLogic({ id: TRACING_SCENE_VIEWER_ID }),
['openTrace', 'selectSpan', 'closeTrace', 'openCompareFlame', 'closeCompareFlame'],
],
})),
actions({
syncUrl: true,
setActiveTracingTab: (tab: 'traces' | 'operations') => ({ tab }),
setDisplayMode: (mode: TracingDisplayMode) => ({ mode }),
}),
reducers({
activeTracingTab: [
'traces' as 'traces' | 'operations',
{
setActiveTracingTab: (_, { tab }) => tab,
},
],
}),
selectors({
breadcrumbs: [
() => [],
(): Breadcrumb[] => [
{
key: 'tracing',
name: 'Tracing',
iconType: 'tracing',
},
],
],
operationsViewEnabled: [
(s) => [s.featureFlags],
(featureFlags: FeatureFlagsSet): boolean => !!featureFlags[FEATURE_FLAGS.TRACING_OPERATIONS_VIEW],
],
// The one user-facing mode: what each result row represents. Spans two states —
// traces/spans is the viewMode filter, operations is the scene tab.
displayMode: [
(s) => [s.operationsViewEnabled, s.activeTracingTab, s.filters],
(
operationsViewEnabled: boolean,
activeTracingTab: 'operations' | 'traces',
filters: TracingFilters
): TracingDisplayMode =>
operationsViewEnabled && activeTracingTab === 'operations' ? 'operations' : filters.viewMode,
],
}),
listeners(({ actions, values }) => ({
// The keyed data logic already captured the interaction and re-ran the query;
// the scene's jobs are the URL write and scene-tab side effects.
handleFilterChange: () => {
actions.syncUrl()
// Keep the Operations aggregate in sync with filters/date while that tab is active.
// Full range: the Operations view always covers the whole selected range, even while
// a (traces-tab) comparison is active.
//
// This MUST run after the data logic's handleFilterChange listener. When a comparison
// is active, that listener synchronously runs runQuery, whose listener fires a windowed
// fetchAggregation(); every fetchAggregation aborts the previous one, so the last
// dispatched wins. The data logic mounts before this scene logic, so its listener runs
// first and the full-range fetch here beats the windowed one deterministically —
// otherwise the Operations table would divide by the narrow compare sub-window.
if (values.activeTracingTab === 'operations') {
actions.fetchAggregation({ fullRange: true })
}
},
setFilters: () => {
actions.syncUrl()
// Bulk restores (URL params, back/forward) route through the data logic's setFilters
// listener → runQuery, which fires a windowed fetchAggregation when a comparison is
// active. Mirror handleFilterChange so the Operations tab keeps its full-range aggregate
// instead of the narrow compare sub-window (same mount-order guarantee applies).
if (values.activeTracingTab === 'operations') {
actions.fetchAggregation({ fullRange: true })
}
},
setActiveTracingTab: ({ tab }) => {
if (tab === 'operations') {
actions.fetchAggregation({ fullRange: true })
} else if (values.compareActive) {
// Returning to the traces tab with a comparison active: the aggregation state
// holds the operations full-range data — refetch the windowed compare version.
actions.fetchAggregation()
}
},
// Maps the one display-mode control onto the two underlying states, with exactly one
// URL write per interaction: setViewMode cascades its own syncUrl (via the data logic's
// handleFilterChange), so this only writes directly when that cascade won't run.
setDisplayMode: ({ mode }) => {
if (mode === 'operations') {
if (values.activeTracingTab !== 'operations') {
actions.setActiveTracingTab('operations')
}
actions.syncUrl()
return
}
if (values.activeTracingTab !== 'traces') {
actions.setActiveTracingTab('traces')
}
if (mode !== values.filters.viewMode) {
actions.setViewMode(mode)
} else {
actions.syncUrl()
}
},
})),
subscriptions(({ actions, values }) => ({
// Feature flags can resolve AFTER the initial URL parse, and urlToAction only re-runs on
// URL changes. Without this, a late-arriving flag leaves the tab and the URL disagreeing:
// enabling it on a ?view=operations page keeps the tab stuck on traces, and disabling it
// mid-session strands the tab on operations. Re-reconcile the tab against the current URL
// whenever the flag flips, then write the URL back through the flag-aware displayMode.
operationsViewEnabled: (enabled) => {
const shouldBeOperations = enabled && router.values.searchParams.view === 'operations'
if (shouldBeOperations === (values.activeTracingTab === 'operations')) {
return
}
actions.setActiveTracingTab(shouldBeOperations ? 'operations' : 'traces')
actions.syncUrl()
},
})),
urlToAction(({ actions, values }) => ({
'/tracing': (_, searchParams) => {
const filtersFromUrl: Record<string, any> = {}
let hasChanges = false
if (searchParams.dateRange) {
try {
const dateRange =
typeof searchParams.dateRange === 'string'
? JSON.parse(searchParams.dateRange)
: searchParams.dateRange
if (!equal(dateRange, values.filters.dateRange)) {
filtersFromUrl.dateRange = dateRange
hasChanges = true
}
} catch {
// Ignore malformed JSON
}
}
if (searchParams.serviceNames) {
const names = parseTagsFilter(searchParams.serviceNames)
if (names && !equal(names, values.filters.serviceNames)) {
filtersFromUrl.serviceNames = names
hasChanges = true
}
} else if (!equal(DEFAULT_SERVICE_NAMES, values.filters.serviceNames)) {
filtersFromUrl.serviceNames = DEFAULT_SERVICE_NAMES
hasChanges = true
}
if (searchParams.filterGroup) {
try {
const filterGroup =
typeof searchParams.filterGroup === 'string'
? JSON.parse(searchParams.filterGroup)
: searchParams.filterGroup
if (!equal(filterGroup, values.filters.filterGroup)) {
filtersFromUrl.filterGroup = filterGroup
hasChanges = true
}
} catch {
// Ignore malformed JSON
}
} else if (!equal(DEFAULT_UNIVERSAL_GROUP_FILTER, values.filters.filterGroup)) {
filtersFromUrl.filterGroup = DEFAULT_UNIVERSAL_GROUP_FILTER
hasChanges = true
}
if (searchParams.orderBy) {
if (searchParams.orderBy !== values.filters.orderBy) {
filtersFromUrl.orderBy = searchParams.orderBy
hasChanges = true
}
}
if (searchParams.orderDirection) {
if (searchParams.orderDirection !== values.filters.orderDirection) {
filtersFromUrl.orderDirection = searchParams.orderDirection
hasChanges = true
}
}
// Legacy `compare=true` links map to the custom-windows preset; everything else
// round-trips through parse/serializeComparison so future modes don't touch this file.
const legacyComparisonFromUrl: TracingComparison | null =
searchParams.compare === 'true' || searchParams.compare === true ? DEFAULT_CUSTOM_COMPARISON : null
let comparisonFromUrl = legacyComparisonFromUrl
// A malformed (or unknown future-mode) comparison param falls back to the legacy
// param, and failing that is ignored rather than clearing an active comparison —
// matching how the dateRange/filterGroup handlers treat malformed JSON.
let comparisonParamInvalid = false
if (searchParams.comparison) {
const parsed = parseComparison(searchParams.comparison)
if (parsed) {
comparisonFromUrl = parsed
} else if (!legacyComparisonFromUrl) {
comparisonParamInvalid = true
}
}
if (
!comparisonParamInvalid &&
serializeComparison(comparisonFromUrl) !== serializeComparison(values.filters.comparison)
) {
filtersFromUrl.comparison = comparisonFromUrl
hasChanges = true
}
// `view` spans two states: 'operations' round-trips the scene tab, 'spans'/absent
// round-trips the viewMode filter. While the URL says operations the viewMode filter
// is left untouched — it has no effect on that view and shouldn't be reset under it.
// The whole operations leg is flag-gated on BOTH the read side (here) and the write
// side (buildUrl, via the flag-aware displayMode selector): gating only one of them
// would make them fight over the tab state.
const operationsEnabled = values.operationsViewEnabled
const operationsFromUrl = operationsEnabled && searchParams.view === 'operations'
if (operationsEnabled && operationsFromUrl !== (values.activeTracingTab === 'operations')) {
actions.setActiveTracingTab(operationsFromUrl ? 'operations' : 'traces')
}
if (!operationsFromUrl) {
const viewModeFromUrl = searchParams.view === 'spans' ? 'spans' : DEFAULT_VIEW_MODE
if (viewModeFromUrl !== values.filters.viewMode) {
filtersFromUrl.viewMode = viewModeFromUrl
hasChanges = true
}
}
const chartTypeFromUrl = searchParams.chart === 'heatmap' ? 'heatmap' : DEFAULT_CHART_TYPE
if (chartTypeFromUrl !== values.filters.chartType) {
filtersFromUrl.chartType = chartTypeFromUrl
hasChanges = true
}
if (hasChanges) {
actions.setFilters(filtersFromUrl)
} else if (!values.hasRunQuery) {
actions.runQuery()
}
// Drawer params. Guarded on a state/URL mismatch so the actionToUrl writes below don't
// loop back through here, and so back/forward opens/closes the drawer correctly.
const traceFromUrl = typeof searchParams.trace === 'string' ? searchParams.trace : null
const spanFromUrl = typeof searchParams.span === 'string' ? searchParams.span : null
if (traceFromUrl && traceFromUrl !== values.selectedTraceId) {
actions.openTrace(traceFromUrl, {
spanId: spanFromUrl,
ts: typeof searchParams.ts === 'string' ? searchParams.ts : null,
})
} else if (traceFromUrl && spanFromUrl !== values.selectedSpanId) {
// Same trace already open, but the URL's span anchor changed (e.g. a shared span
// link to a trace the user already has open) — move the selection, don't reopen.
actions.selectSpan(spanFromUrl)
} else if (!traceFromUrl && values.selectedTraceId) {
actions.closeTrace()
}
},
})),
trackedActionToUrl(({ values }) => {
// The drawer params (trace/span/ts) live alongside the filter params. They're written by
// their own handlers below (which must NOT re-run the list query), and preserved by
// buildUrl so a filter change doesn't silently strip an open drawer from the URL.
const drawerParams = (): Params => {
const params: Params = {}
if (values.selectedTraceId) {
params.trace = values.selectedTraceId
if (values.selectedSpanId) {
params.span = values.selectedSpanId
}
if (values.selectedTraceTs) {
params.ts = values.selectedTraceTs
}
}
return params
}
const buildUrl = (): [string, Params, Record<string, any>, { replace: boolean }] => {
const searchParams: Params = { ...drawerParams() }
if (!equal(values.filters.dateRange, DEFAULT_DATE_RANGE)) {
searchParams.dateRange = JSON.stringify(values.filters.dateRange)
}
if (!equal(values.filters.serviceNames, DEFAULT_SERVICE_NAMES)) {
searchParams.serviceNames = values.filters.serviceNames
}
if (!equal(values.filters.filterGroup, DEFAULT_UNIVERSAL_GROUP_FILTER)) {
searchParams.filterGroup = JSON.stringify(values.filters.filterGroup)
}
if (values.filters.orderBy !== DEFAULT_ORDER_BY) {
searchParams.orderBy = values.filters.orderBy
}
if (values.filters.orderDirection !== DEFAULT_ORDER_DIRECTION) {
searchParams.orderDirection = values.filters.orderDirection
}
const comparisonParam = serializeComparison(values.filters.comparison)
if (comparisonParam) {
searchParams.comparison = comparisonParam
}
// displayMode already folds in the flag gate and resolves to viewMode off-operations.
if (values.displayMode !== DEFAULT_VIEW_MODE) {
searchParams.view = values.displayMode
}
if (values.filters.chartType !== DEFAULT_CHART_TYPE) {
searchParams.chart = values.filters.chartType
}
return [router.values.location.pathname, searchParams, router.values.hashParams, { replace: true }]
}
// Merge the drawer's state into the CURRENT url (filters untouched, no query re-run).
// Opening/closing pushes a history entry (back closes the drawer); span selection replaces
// (clicking around a waterfall shouldn't spam history).
const drawerUrl = (replace: boolean): [string, Params, Record<string, any>, { replace: boolean }] => {
const { trace, span, ts, ...rest } = router.values.searchParams
return [
router.values.location.pathname,
{ ...rest, ...drawerParams() },
router.values.hashParams,
{ replace },
]
}
return {
syncUrl: () => buildUrl(),
openTrace: () => drawerUrl(false),
selectSpan: () => drawerUrl(true),
closeTrace: () => drawerUrl(false),
}
}),
])