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.
659 lines
27 KiB
TypeScript
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),
|
|
}
|
|
}),
|
|
])
|