Files
zeekay e4a276e409 brand: no upstream art, no third-party image hosts, the SDK is ours everywhere
Workflow template cards, the template choosers, the auth wallpaper, insight
previews, partner pages, SDK logos and every fixture stop pulling posthog's
cloudinary; the CSP stops allowing it. The mascot easter egg leaves with its
sprite package. insights-js resolves to @hanzo/insights for every consumer —
the react wrapper's peer included, via a workspace package that carries the
name — and its rrweb siblings map to our published forks. Icon names that
outran our icon release alias to shipped glyphs.
2026-08-06 11:21:45 -07:00

1489 lines
64 KiB
TypeScript

import {
MakeLogicType,
actions,
afterMount,
connect,
events,
kea,
key,
listeners,
path,
props,
reducers,
selectors,
} from 'kea'
import { loaders } from 'kea-loaders'
import insights from 'insights-js'
import { toast } from '@hanzo/elements'
import api from 'lib/api'
import { dataColorVars } from 'lib/colors'
import { FEATURE_FLAGS } from 'lib/constants'
import { dayjs } from 'lib/dayjs'
import { featureFlagLogic, type FeatureFlagsSet } from 'lib/logic/featureFlagLogic'
import { humanFriendlyDetailedTime } from 'lib/utils/datetime'
import { AggregatedSpanRow, SpanTreeNode } from '~/queries/schema/schema-general'
import { PropertyGroupFilter } from '~/types'
import type { DateRange } from '../../../frontend/src/queries/schema/schema-general'
import type { UniversalFiltersGroup } from '../../../frontend/src/types'
import {
type DurationHistogramRow,
type LatencyHeatmapRow,
pivotDurationHistogram,
pivotLatencyHeatmap,
type TracingDurationHistogramData,
type TracingLatencyHeatmapData,
type VisibleDurationRange,
visibleDurationRange,
} from './durationBuckets'
import { type HeatmapBrushSelection, heatmapBrushToFilters } from './heatmapBrush'
import { traceLookupDateRange } from './traceLinks'
import {
type TracingFilters,
type TracingOrderBy,
TRACING_SCENE_VIEWER_ID,
tracingFiltersLogic,
} from './tracingFiltersLogic'
import type { OverlayWindow, TimeComparison, TracingOrderDirection, TracingViewMode } from './tracingFiltersLogic'
import type { Span } from './types'
export interface SparklineRow {
time: string
service: string
count: number
}
export interface TracingSparklineData {
data: { name: string; values: number[]; color: string }[]
dates: string[]
labels: string[]
}
export interface VisibleRowRange {
startIndex: number
stopIndex: number
}
export interface VisibleSpanTimeRange {
date_from: string
date_to: string
}
const DEFAULT_PAGE_SIZE = 100
// Matches the backend's hard cap (_ROW_LIMIT) for span aggregation. The Operations tab needs
// the full set for client-side sort/filter, unlike the smaller default the endpoint serves agents.
const OPERATIONS_AGGREGATION_LIMIT = 5000
export const PREFETCH_SPANS = 20
export const NEW_QUERY_STARTED_ERROR_MESSAGE = 'new query started' as const
export function isUserInitiatedError(error: unknown): boolean {
const errorStr = String(error).toLowerCase()
return error === NEW_QUERY_STARTED_ERROR_MESSAGE || errorStr.includes('abort')
}
// A ts hint (from a shared/cold link) bounds the lookup tightly around the trace instead of the
// scene's current date range — the table is time-keyed, so this is what keeps an id lookup from
// scanning the whole window. Guard validity: a hand-edited/corrupted ts would otherwise make dayjs
// throw on toISOString().
function resolveTraceLookupRange(
ts: string | null | undefined,
utcDateRange: { date_from?: string | null; date_to?: string | null }
): { date_from?: string | null; date_to?: string | null } {
if (ts && dayjs(ts).isValid()) {
return traceLookupDateRange(ts)
}
return {
date_from: utcDateRange.date_from ?? '-24h',
date_to: utcDateRange.date_to ?? undefined,
}
}
function captureTracingResults(count: number, queryType: 'spans' | 'aggregation'): void {
if (count === 0) {
insights.capture('tracing no results returned', { query_type: queryType })
} else {
insights.capture('tracing results returned', { count, query_type: queryType })
}
}
export interface TracingDataLogicProps {
id: string
// Run the first query on mount. Embedded viewers set this; the /tracing scene leaves it
// off because its first query is driven by urlToAction (which may first restore filters
// from the URL). Mirrors the LogsViewer `autoLoad` prop.
autoLoad?: boolean
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface tracingDataLogicValues {
featureFlags: FeatureFlagsSet; // featureFlagLogic
compareActive: boolean; // tracingFiltersLogic
currentWindowMs: OverlayWindow; // tracingFiltersLogic
filters: TracingFilters; // tracingFiltersLogic
orderBy: TracingOrderBy; // tracingFiltersLogic
previousWindowMs: OverlayWindow; // tracingFiltersLogic
queryFilterGroup: UniversalFiltersGroup; // tracingFiltersLogic
sparklineWindowMs: OverlayWindow; // tracingFiltersLogic
timeComparison: TimeComparison | null; // tracingFiltersLogic
utcDateRange: {
date_from: string | null | undefined;
date_to: string | null | undefined;
}; // tracingFiltersLogic
aggregation: {
current: AggregatedSpanRow[];
previous: AggregatedSpanRow[] | null;
};
aggregationAbortController: AbortController | null;
aggregationLoading: boolean;
durationHistogramAbortController: AbortController | null;
durationHistogramData: TracingDurationHistogramData;
durationHistogramLoading: boolean;
hasMoreToLoad: boolean;
hasRunQuery: boolean;
isDurationMode: boolean;
lastAggregationWindow: {
currentEndMs: number;
currentStartMs: number;
previousEndMs: number;
previousStartMs: number;
} | null;
latencyHeatmapAbortController: AbortController | null;
latencyHeatmapData: TracingLatencyHeatmapData;
latencyHeatmapLoading: boolean;
listRowDurations: number[];
listRows: Span[];
matchingCounts: {
count: number;
traceCount: number;
};
matchingCountsAbortController: AbortController | null;
matchingCountsLoading: boolean;
nextCursor: string | null;
rawDurationHistogram: DurationHistogramRow[];
rawDurationHistogramLoading: boolean;
rawLatencyHeatmap: LatencyHeatmapRow[];
rawLatencyHeatmapLoading: boolean;
rawSparklineData: SparklineRow[];
rawSparklineDataLoading: boolean;
showHeatmap: boolean;
spanTree: {
current: SpanTreeNode[];
previous: SpanTreeNode[] | null;
spanName: string | null;
};
spanTreeAbortController: AbortController | null;
spanTreeLoading: boolean;
spans: Span[];
spansAbortController: AbortController | null;
spansLoading: boolean;
sparklineAbortController: AbortController | null;
sparklineData: TracingSparklineData;
sparklineLoading: boolean;
totalMatchingFilters: number;
traceLoadContext: {
traceId: string;
ts?: string | null;
} | null;
traceSpans: Span[];
traceSpansHasMore: boolean;
traceSpansLoading: boolean;
traceSpansLoadingMore: boolean;
traceSpansNextOffset: number;
visibleRowDateRange: VisibleSpanTimeRange | null;
visibleRowDurationRange: VisibleDurationRange | null;
visibleRowRange: VisibleRowRange | null;
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface tracingDataLogicActions {
setFeatureFlags: (flags: string[], variants: Record<string, boolean | string>, payloads?: import("lib/logic/featureFlagLogic").FeatureFlagPayloads | undefined) => {
flags: string[];
payloads: import("lib/logic/featureFlagLogic").FeatureFlagPayloads;
variants: Record<string, boolean | string>;
}; // featureFlagLogic
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
applyHeatmapBrush: (selection: HeatmapBrushSelection) => {
selection: HeatmapBrushSelection;
};
cancelInProgressAggregation: (controller: AbortController | null) => {
controller: AbortController | null;
};
cancelInProgressDurationHistogram: (controller: AbortController | null) => {
controller: AbortController | null;
};
cancelInProgressLatencyHeatmap: (controller: AbortController | null) => {
controller: AbortController | null;
};
cancelInProgressMatchingCounts: (controller: AbortController | null) => {
controller: AbortController | null;
};
cancelInProgressSpanTree: (controller: AbortController | null) => {
controller: AbortController | null;
};
cancelInProgressSpans: (controller: AbortController | null) => {
controller: AbortController | null;
};
cancelInProgressSparkline: (controller: AbortController | null) => {
controller: AbortController | null;
};
clearSpans: () => {
value: true;
};
clearSpansFailure: (error: string, errorObject?: any) => {
error: string;
errorObject?: any;
};
clearSpansSuccess: (spans: never[], payload?: {
value: true;
}) => {
spans: never[];
payload?: {
value: true;
};
};
fetchAggregation: (options?: {
fullRange?: boolean;
} | null) => {
fullRange?: boolean;
} | null;
fetchAggregationFailure: (error: string, errorObject?: any) => {
error: string;
errorObject?: any;
};
fetchAggregationSuccess: (aggregation: {
current: AggregatedSpanRow[];
previous: AggregatedSpanRow[] | null;
}, payload?: {
fullRange?: boolean;
} | null) => {
aggregation: {
current: AggregatedSpanRow[];
previous: AggregatedSpanRow[] | null;
};
payload?: {
fullRange?: boolean;
} | null;
};
fetchDurationHistogram: () => any;
fetchDurationHistogramFailure: (error: string, errorObject?: any) => {
error: string;
errorObject?: any;
};
fetchDurationHistogramSuccess: (rawDurationHistogram: {
bucket_ns: number;
count: number;
service: string;
}[], payload?: any) => {
rawDurationHistogram: {
bucket_ns: number;
count: number;
service: string;
}[];
payload?: any;
};
fetchLatencyHeatmap: () => any;
fetchLatencyHeatmapFailure: (error: string, errorObject?: any) => {
error: string;
errorObject?: any;
};
fetchLatencyHeatmapSuccess: (rawLatencyHeatmap: {
bucket_ns: number;
count: number;
time: string;
}[], payload?: any) => {
rawLatencyHeatmap: {
bucket_ns: number;
count: number;
time: string;
}[];
payload?: any;
};
fetchMatchingCounts: () => any;
fetchMatchingCountsFailure: (error: string, errorObject?: any) => {
error: string;
errorObject?: any;
};
fetchMatchingCountsSuccess: (matchingCounts: {
count: number;
traceCount: number;
}, payload?: any) => {
matchingCounts: {
count: number;
traceCount: number;
};
payload?: any;
};
fetchNextPage: () => {
value: true;
};
fetchNextPageFailure: (error: string, errorObject?: any) => {
error: string;
errorObject?: any;
};
fetchNextPageSuccess: (spans: Span[], payload?: {
value: true;
}) => {
spans: Span[];
payload?: {
value: true;
};
};
fetchSpanTree: (params: {
serviceName: string;
spanName: string;
}) => {
spanName: string;
serviceName: string;
};
fetchSpanTreeFailure: (error: string, errorObject?: any) => {
error: string;
errorObject?: any;
};
fetchSpanTreeSuccess: (spanTree: {
current: SpanTreeNode[];
previous: SpanTreeNode[] | null;
spanName: string;
}, payload?: {
spanName: string;
serviceName: string;
}) => {
spanTree: {
current: SpanTreeNode[];
previous: SpanTreeNode[] | null;
spanName: string;
};
payload?: {
spanName: string;
serviceName: string;
};
};
fetchSpans: () => any;
fetchSpansFailure: (error: string, errorObject?: any) => {
error: string;
errorObject?: any;
};
fetchSpansSuccess: (spans: Span[], payload?: any) => {
spans: Span[];
payload?: any;
};
fetchSparkline: () => any;
fetchSparklineFailure: (error: string, errorObject?: any) => {
error: string;
errorObject?: any;
};
fetchSparklineSuccess: (rawSparklineData: {
count: number;
service: string;
time: string;
}[], payload?: any) => {
rawSparklineData: {
count: number;
service: string;
time: string;
}[];
payload?: any;
};
handleFilterChange: (filterType: string, extraProps?: Record<string, unknown>) => {
extraProps: Record<string, unknown> | undefined;
filterType: string;
};
loadMoreTraceSpans: () => {
value: true;
};
loadMoreTraceSpansFailure: (error: string, errorObject?: any) => {
error: string;
errorObject?: any;
};
loadMoreTraceSpansSuccess: (traceSpans: Span[], payload?: {
value: true;
}) => {
traceSpans: Span[];
payload?: {
value: true;
};
};
loadTraceSpans: ({ traceId, ts }: {
traceId: string;
ts?: string | null;
}) => {
traceId: string;
ts?: string | null;
};
loadTraceSpansFailure: (error: string, errorObject?: any) => {
error: string;
errorObject?: any;
};
loadTraceSpansSuccess: (traceSpans: Span[], payload?: {
traceId: string;
ts?: string | null;
}) => {
traceSpans: Span[];
payload?: {
traceId: string;
ts?: string | null;
};
};
runQuery: () => {
value: true;
};
setAggregationAbortController: (controller: AbortController | null) => {
controller: AbortController | null;
};
setDurationHistogramAbortController: (controller: AbortController | null) => {
controller: AbortController | null;
};
setHasMoreToLoad: (hasMore: boolean) => {
hasMore: boolean;
};
setLastAggregationWindow: (window: {
currentEndMs: number;
currentStartMs: number;
previousEndMs: number;
previousStartMs: number;
}) => {
window: {
currentEndMs: number;
currentStartMs: number;
previousEndMs: number;
previousStartMs: number;
};
};
setLatencyHeatmapAbortController: (controller: AbortController | null) => {
controller: AbortController | null;
};
setMatchingCountsAbortController: (controller: AbortController | null) => {
controller: AbortController | null;
};
setNextCursor: (cursor: string | null) => {
cursor: string | null;
};
setSpanTreeAbortController: (controller: AbortController | null) => {
controller: AbortController | null;
};
setSpansAbortController: (controller: AbortController | null) => {
controller: AbortController | null;
};
setSparklineAbortController: (controller: AbortController | null) => {
controller: AbortController | null;
};
setTracePagination: (hasMore: boolean, nextOffset: number | null) => {
hasMore: boolean;
nextOffset: number | null;
};
setVisibleRowRange: (startIndex: number, stopIndex: number) => {
startIndex: number;
stopIndex: number;
};
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface tracingDataLogicMeta {
key: string;
__keaTypeGenInternalSelectorTypes: {
sparklineData: (rawSparklineData: SparklineRow[]) => TracingSparklineData;
totalMatchingFilters: (matchingCounts: {
count: number;
traceCount: number;
}, filters: TracingFilters) => number;
durationHistogramData: (rawDurationHistogram: DurationHistogramRow[]) => TracingDurationHistogramData;
latencyHeatmapData: (rawLatencyHeatmap: LatencyHeatmapRow[]) => TracingLatencyHeatmapData;
showHeatmap: (filters: TracingFilters, compareActive: boolean, featureFlags: FeatureFlagsSet) => boolean;
listRows: (spans: Span[], filters: TracingFilters) => Span[];
listRowDurations: (listRows: Span[]) => number[];
isDurationMode: (filters: TracingFilters, compareActive: boolean) => boolean;
visibleRowDurationRange: (visibleRowRange: VisibleRowRange | null, listRowDurations: number[], isDurationMode: boolean) => VisibleDurationRange | null;
visibleRowDateRange: (visibleRowRange: VisibleRowRange | null, listRows: Span[], orderBy: TracingOrderBy) => VisibleSpanTimeRange | null;
};
}
export type tracingDataLogicType = MakeLogicType<tracingDataLogicValues, tracingDataLogicActions, TracingDataLogicProps, tracingDataLogicMeta>;
export const tracingDataLogic = kea<tracingDataLogicType>([
props({ id: TRACING_SCENE_VIEWER_ID, autoLoad: false } as TracingDataLogicProps),
key((props) => props.id),
path((key) => ['products', 'tracing', 'frontend', 'tracingDataLogic', key]),
connect(({ id }: TracingDataLogicProps) => ({
values: [
tracingFiltersLogic({ id }),
[
'filters',
'orderBy',
'utcDateRange',
'queryFilterGroup',
'sparklineWindowMs',
'currentWindowMs',
'previousWindowMs',
'compareActive',
'timeComparison',
],
featureFlagLogic,
['featureFlags'],
],
actions: [
tracingFiltersLogic({ id }),
[
'setDateRange',
'setServiceNames',
'setFilterGroup',
'setSort',
'setViewMode',
'setChartType',
'setComparison',
'updateComparisonWindows',
'setFilters',
],
featureFlagLogic,
['setFeatureFlags'],
],
})),
actions({
// Analytics + re-query on a user filter interaction. Dispatched by the filter-action
// listeners below; the scene logic additionally listens to this to sync the URL.
// Mirrors the logs viewer's handleQueryChange.
handleFilterChange: (filterType: string, extraProps?: Record<string, unknown>) => ({ filterType, extraProps }),
// A completed 2D brush on the latency heatmap — maps to a date range + duration chips.
applyHeatmapBrush: (selection: HeatmapBrushSelection) => ({ selection }),
runQuery: true,
fetchNextPage: true,
loadMoreTraceSpans: true,
setTracePagination: (hasMore: boolean, nextOffset: number | null) => ({ hasMore, nextOffset }),
clearSpans: true,
cancelInProgressSpans: (controller: AbortController | null) => ({ controller }),
cancelInProgressSparkline: (controller: AbortController | null) => ({ controller }),
cancelInProgressMatchingCounts: (controller: AbortController | null) => ({ controller }),
cancelInProgressDurationHistogram: (controller: AbortController | null) => ({ controller }),
cancelInProgressLatencyHeatmap: (controller: AbortController | null) => ({ controller }),
cancelInProgressAggregation: (controller: AbortController | null) => ({ controller }),
cancelInProgressSpanTree: (controller: AbortController | null) => ({ controller }),
setSpansAbortController: (controller: AbortController | null) => ({ controller }),
setSparklineAbortController: (controller: AbortController | null) => ({ controller }),
setMatchingCountsAbortController: (controller: AbortController | null) => ({ controller }),
setDurationHistogramAbortController: (controller: AbortController | null) => ({ controller }),
setLatencyHeatmapAbortController: (controller: AbortController | null) => ({ controller }),
setAggregationAbortController: (controller: AbortController | null) => ({ controller }),
setSpanTreeAbortController: (controller: AbortController | null) => ({ controller }),
setHasMoreToLoad: (hasMore: boolean) => ({ hasMore }),
setNextCursor: (cursor: string | null) => ({ cursor }),
setVisibleRowRange: (startIndex: number, stopIndex: number) => ({ startIndex, stopIndex }),
/**
* Snapshot the resolved time windows used for the last aggregation fetch.
* The drill-down flame query reads this snapshot instead of recomputing from
* the (potentially shifted) `-1h`-style relative date range, so its numbers
* line up with the row the user clicked.
*/
setLastAggregationWindow: (window: {
currentStartMs: number
currentEndMs: number
previousStartMs: number
previousEndMs: number
}) => ({ window }),
}),
reducers({
spansAbortController: [
null as AbortController | null,
{ setSpansAbortController: (_, { controller }) => controller },
],
sparklineAbortController: [
null as AbortController | null,
{ setSparklineAbortController: (_, { controller }) => controller },
],
matchingCountsAbortController: [
null as AbortController | null,
{ setMatchingCountsAbortController: (_, { controller }) => controller },
],
durationHistogramAbortController: [
null as AbortController | null,
{ setDurationHistogramAbortController: (_, { controller }) => controller },
],
latencyHeatmapAbortController: [
null as AbortController | null,
{ setLatencyHeatmapAbortController: (_, { controller }) => controller },
],
aggregationAbortController: [
null as AbortController | null,
{ setAggregationAbortController: (_, { controller }) => controller },
],
spanTreeAbortController: [
null as AbortController | null,
{ setSpanTreeAbortController: (_, { controller }) => controller },
],
lastAggregationWindow: [
null as null | {
currentStartMs: number
currentEndMs: number
previousStartMs: number
previousEndMs: number
},
{ setLastAggregationWindow: (_, { window }) => window },
],
hasRunQuery: [
false as boolean,
{
fetchSpansSuccess: () => true,
fetchSpansFailure: () => true,
},
],
spansLoading: [
false as boolean,
{
fetchSpans: () => true,
fetchSpansSuccess: () => false,
// A superseded query is aborted by the newer one that already re-set loading true;
// keep loading so the list holds its spinner instead of flashing "No spans found".
fetchSpansFailure: (state, { error }) => (isUserInitiatedError(error) ? state : false),
fetchNextPage: () => true,
fetchNextPageSuccess: () => false,
fetchNextPageFailure: (state, { error }) => (isUserInitiatedError(error) ? state : false),
},
],
sparklineLoading: [
false as boolean,
{
fetchSparkline: () => true,
fetchSparklineSuccess: () => false,
fetchSparklineFailure: () => false,
},
],
durationHistogramLoading: [
false as boolean,
{
fetchDurationHistogram: () => true,
fetchDurationHistogramSuccess: () => false,
fetchDurationHistogramFailure: () => false,
},
],
latencyHeatmapLoading: [
false as boolean,
{
fetchLatencyHeatmap: () => true,
fetchLatencyHeatmapSuccess: () => false,
fetchLatencyHeatmapFailure: () => false,
},
],
aggregationLoading: [
false as boolean,
{
fetchAggregation: () => true,
fetchAggregationSuccess: () => false,
fetchAggregationFailure: (state, { error }) => (isUserInitiatedError(error) ? state : false),
},
],
spanTreeLoading: [
false as boolean,
{
fetchSpanTree: () => true,
fetchSpanTreeSuccess: () => false,
fetchSpanTreeFailure: () => false,
},
],
hasMoreToLoad: [
true as boolean,
{
setHasMoreToLoad: (_, { hasMore }) => hasMore,
clearSpans: () => true,
},
],
nextCursor: [
null as string | null,
{
setNextCursor: (_, { cursor }) => cursor,
clearSpans: () => null,
},
],
visibleRowRange: [
null as VisibleRowRange | null,
{
setVisibleRowRange: (_, { startIndex, stopIndex }) => ({ startIndex, stopIndex }),
clearSpans: () => null,
},
],
// Whether the open trace has more spans than the loaded pages (drives the waterfall's
// infinite scroll). Reset on every fresh trace open so a small trace can't inherit a
// previous trace's "load more" affordance.
traceSpansHasMore: [
false as boolean,
{
setTracePagination: (_, { hasMore }) => hasMore,
loadTraceSpans: () => false,
},
],
// Offset to request for the next waterfall page (count of spans already loaded for the trace).
traceSpansNextOffset: [
0 as number,
{
setTracePagination: (_, { nextOffset }) => nextOffset ?? 0,
loadTraceSpans: () => 0,
},
],
// The trace + ts the loaded spans belong to, so `loadMoreTraceSpans` can refetch the next
// page with the same lookup window without re-deriving it from a since-shifted date range.
traceLoadContext: [
null as { traceId: string; ts?: string | null } | null,
{
loadTraceSpans: (_, { traceId, ts }) => ({ traceId, ts }),
clearSpans: () => null,
},
],
// Separate from the loader's own `traceSpansLoading` so paging in more spans shows a small
// bottom spinner rather than the drawer's full-trace overlay (see `isLoadingFullTrace`).
traceSpansLoadingMore: [
false as boolean,
{
loadMoreTraceSpans: () => true,
loadMoreTraceSpansSuccess: () => false,
loadMoreTraceSpansFailure: () => false,
// A fresh trace fetch must show the full-trace overlay even if a stale load-more is
// still in flight.
loadTraceSpans: () => false,
},
],
}),
loaders(({ values, actions, cache }) => ({
spans: [
[] as Span[],
{
clearSpans: () => [],
fetchSpans: async () => {
const controller = new AbortController()
actions.cancelInProgressSpans(controller)
const response = await api.tracing.listSpans(
{
dateRange: values.utcDateRange,
orderBy: values.filters.orderBy,
orderDirection: values.filters.orderDirection,
serviceNames:
values.filters.serviceNames.length > 0 ? values.filters.serviceNames : undefined,
filterGroup: values.queryFilterGroup as PropertyGroupFilter,
prefetchSpans: PREFETCH_SPANS,
flatSpans: values.filters.viewMode === 'spans',
limit: DEFAULT_PAGE_SIZE,
},
controller.signal
)
actions.setSpansAbortController(null)
actions.setHasMoreToLoad(!!response.hasMore)
actions.setNextCursor(response.nextCursor ?? null)
return response.results as Span[]
},
fetchNextPage: async () => {
if (!values.hasMoreToLoad) {
return values.spans
}
const controller = new AbortController()
actions.cancelInProgressSpans(controller)
// Duration ordering paginates by offset (it has no keyset cursor); timestamp
// ordering uses the `after` cursor. Offset is the count of rows already shown.
const pagination =
values.filters.orderBy === 'duration'
? { offset: values.listRows.length }
: { after: values.nextCursor ?? undefined }
const response = await api.tracing.listSpans(
{
dateRange: values.utcDateRange,
orderBy: values.filters.orderBy,
orderDirection: values.filters.orderDirection,
serviceNames:
values.filters.serviceNames.length > 0 ? values.filters.serviceNames : undefined,
filterGroup: values.queryFilterGroup as PropertyGroupFilter,
prefetchSpans: PREFETCH_SPANS,
flatSpans: values.filters.viewMode === 'spans',
limit: DEFAULT_PAGE_SIZE,
...pagination,
},
controller.signal
)
actions.setSpansAbortController(null)
actions.setHasMoreToLoad(!!response.hasMore)
actions.setNextCursor(response.nextCursor ?? null)
return [...values.spans, ...(response.results as Span[])]
},
},
],
traceSpans: [
[] as Span[],
{
loadTraceSpans: async ({ traceId, ts }: { traceId: string; ts?: string | null }): Promise<Span[]> => {
const response = await api.tracing.getTrace(traceId, {
dateRange: resolveTraceLookupRange(ts, values.utcDateRange),
serviceNames: values.filters.serviceNames.length > 0 ? values.filters.serviceNames : undefined,
filterGroup: values.queryFilterGroup as PropertyGroupFilter,
})
actions.setTracePagination(!!response.hasMore, response.nextOffset ?? null)
return response.results as Span[]
},
// Infinite scroll: page in the next batch of the open trace's spans (earliest first)
// and append. Guarded so a trailing scroll event after the last page is a no-op.
loadMoreTraceSpans: async (): Promise<Span[]> => {
if (!values.traceSpansHasMore || !values.traceLoadContext) {
return values.traceSpans
}
const { traceId, ts } = values.traceLoadContext
const response = await api.tracing.getTrace(traceId, {
dateRange: resolveTraceLookupRange(ts, values.utcDateRange),
serviceNames: values.filters.serviceNames.length > 0 ? values.filters.serviceNames : undefined,
filterGroup: values.queryFilterGroup as PropertyGroupFilter,
offset: values.traceSpansNextOffset,
})
// Bail if a new trace was opened mid-fetch — appending this stale page to the new
// trace's spans would corrupt the waterfall.
if (values.traceLoadContext?.traceId !== traceId) {
return values.traceSpans
}
// Dedupe against what's already loaded: the waterfall keys its tree by span_id,
// so a page that overlaps prior pages wouldn't grow the rendered rows but would
// still balloon this array (and pin the infinite-scroll trigger at the bottom,
// spinning the CPU). If a page adds nothing new, stop paging.
const seen = new Set(values.traceSpans.map((s) => s.span_id))
const newSpans = (response.results as Span[]).filter((s) => !seen.has(s.span_id))
if (newSpans.length === 0) {
actions.setTracePagination(false, null)
return values.traceSpans
}
actions.setTracePagination(!!response.hasMore, response.nextOffset ?? null)
return [...values.traceSpans, ...newSpans]
},
},
],
spanTree: [
{
spanName: null as string | null,
current: [] as SpanTreeNode[],
previous: null as SpanTreeNode[] | null,
},
{
fetchSpanTree: async (params: { spanName: string; serviceName: string }) => {
// Abort any in-flight tree fetch so rapid row-clicks can't deliver a stale
// response that overwrites the newer one — matches the pattern used by
// fetchSpans / fetchSparkline / fetchAggregation.
const controller = new AbortController()
actions.cancelInProgressSpanTree(controller)
// Use the snapshotted window from the last aggregation fetch when available
// so the drill-down numbers align with the row the user clicked. Fall back
// to the live computed window only on a cold start (e.g. deep link before
// any aggregation has run).
const snapshot = values.lastAggregationWindow
const currentStartMs = snapshot?.currentStartMs ?? values.currentWindowMs.startMs
const currentEndMs = snapshot?.currentEndMs ?? values.currentWindowMs.endMs
const previousStartMs = snapshot?.previousStartMs ?? values.previousWindowMs.startMs
const response = await api.tracing.tree(
{
spanName: params.spanName,
serviceName: params.serviceName,
dateRange: {
date_from: new Date(currentStartMs).toISOString(),
date_to: new Date(currentEndMs).toISOString(),
},
serviceNames:
values.filters.serviceNames.length > 0 ? values.filters.serviceNames : undefined,
filterGroup: values.queryFilterGroup as PropertyGroupFilter,
compareFilter: {
compare: values.timeComparison !== null,
compare_to: new Date(previousStartMs).toISOString(),
},
},
controller.signal
)
actions.setSpanTreeAbortController(null)
return {
spanName: params.spanName,
current: response.results ?? [],
previous: response.compare ?? null,
}
},
},
],
aggregation: [
{ current: [] as AggregatedSpanRow[], previous: null as AggregatedSpanRow[] | null },
{
// `fullRange` (the Operations tab) ignores any active comparison: the aggregate
// covers the whole selected range with no compare period, matching the rate
// denominator the OperationsTable divides by.
fetchAggregation: async (options?: { fullRange?: boolean } | null) => {
const controller = new AbortController()
actions.cancelInProgressAggregation(controller)
const fullRange = options?.fullRange ?? false
const currentWindow = fullRange ? values.sparklineWindowMs : values.currentWindowMs
const compare = !fullRange && values.timeComparison !== null
// Snapshot the resolved windows so the drill-down flame query uses the
// same absolute timestamps as this aggregation, even if the user's
// relative `-1h` range has since shifted forward in time. Dispatched
// BEFORE the await so a concurrent `fetchSpanTree` (e.g. fired from the
// same `updateComparisonWindows` listener) reads the new window, not the
// old. Full-range (Operations) fetches skip it — the flame drill-down
// only exists on the traces tab and must stay aligned with the compare
// aggregation, not the operations one.
const window = {
currentStartMs: currentWindow.startMs,
currentEndMs: currentWindow.endMs,
previousStartMs: values.previousWindowMs.startMs,
previousEndMs: values.previousWindowMs.endMs,
}
if (!fullRange) {
actions.setLastAggregationWindow(window)
}
const response = await api.tracing.aggregate(
{
dateRange: {
date_from: new Date(window.currentStartMs).toISOString(),
date_to: new Date(window.currentEndMs).toISOString(),
},
serviceNames:
values.filters.serviceNames.length > 0 ? values.filters.serviceNames : undefined,
filterGroup: values.queryFilterGroup as PropertyGroupFilter,
compareFilter: {
compare,
compare_to: new Date(window.previousStartMs).toISOString(),
},
// The Operations table sorts/filters the full result set client-side, so
// request the endpoint's hard cap rather than its small default page.
limit: OPERATIONS_AGGREGATION_LIMIT,
},
controller.signal
)
actions.setAggregationAbortController(null)
return {
current: response.results ?? [],
previous: response.compare ?? null,
}
},
},
],
rawSparklineData: [
[] as SparklineRow[],
{
fetchSparkline: async () => {
// The sparkline counts root spans in 'traces' mode and every span in 'spans' mode
// (via rootSpans below), so it depends on the data scope (date range, services,
// filters) AND the view mode — but not on sort or compare. Skip the re-fetch (and
// its spinner overlay) only when a sort/compare toggle re-runs the query without
// changing scope or view mode.
const scopeKey = JSON.stringify([
values.utcDateRange,
values.filters.serviceNames,
values.queryFilterGroup,
values.filters.viewMode,
])
if (scopeKey === cache.sparklineScope) {
return values.rawSparklineData
}
const controller = new AbortController()
actions.cancelInProgressSparkline(controller)
const response = await api.tracing.sparkline(
{
dateRange: values.utcDateRange,
serviceNames:
values.filters.serviceNames.length > 0 ? values.filters.serviceNames : undefined,
filterGroup: values.queryFilterGroup as PropertyGroupFilter,
rootSpans: values.filters.viewMode === 'traces',
},
controller.signal
)
actions.setSparklineAbortController(null)
// Record the scope only after a successful fetch, so a failed/aborted request retries.
cache.sparklineScope = scopeKey
return response.results
},
},
],
matchingCounts: [
{ count: 0, traceCount: 0 } as { count: number; traceCount: number },
{
fetchMatchingCounts: async () => {
// The count is view-mode-independent — it returns both span and trace counts in one
// response, and the label selects which to show. So a Traces/Spans (or sort/compare)
// toggle that re-runs the query must not re-hit the endpoint; only the data scope
// (date range, services, filters) changes the result. Skip the fetch when unchanged.
const scopeKey = JSON.stringify([
values.utcDateRange,
values.filters.serviceNames,
values.queryFilterGroup,
])
if (scopeKey === cache.matchingCountsScope) {
return values.matchingCounts
}
const controller = new AbortController()
actions.cancelInProgressMatchingCounts(controller)
const response = await api.tracing.count(
{
dateRange: values.utcDateRange,
serviceNames:
values.filters.serviceNames.length > 0 ? values.filters.serviceNames : undefined,
filterGroup: values.queryFilterGroup as PropertyGroupFilter,
},
controller.signal
)
actions.setMatchingCountsAbortController(null)
// Record the scope only after a successful fetch, so a failed/aborted request retries.
cache.matchingCountsScope = scopeKey
return response
},
},
],
rawLatencyHeatmap: [
[] as LatencyHeatmapRow[],
{
fetchLatencyHeatmap: async () => {
// Same scope semantics as the sparkline: the heatmap depends on the data scope
// and the view mode (root spans vs every span), but not on sort or compare.
// Skip the re-fetch when a sort toggle re-runs the query without changing scope.
const scopeKey = JSON.stringify([
values.utcDateRange,
values.filters.serviceNames,
values.queryFilterGroup,
values.filters.viewMode,
])
if (scopeKey === cache.latencyHeatmapScope) {
return values.rawLatencyHeatmap
}
const controller = new AbortController()
actions.cancelInProgressLatencyHeatmap(controller)
const response = await api.tracing.latencyHeatmap(
{
dateRange: values.utcDateRange,
serviceNames:
values.filters.serviceNames.length > 0 ? values.filters.serviceNames : undefined,
filterGroup: values.queryFilterGroup as PropertyGroupFilter,
// Match the population the list shows: root spans in Traces view,
// every matching span in Spans view.
rootSpans: values.filters.viewMode === 'traces',
},
controller.signal
)
actions.setLatencyHeatmapAbortController(null)
// Record the scope only after a successful fetch, so a failed/aborted request retries.
cache.latencyHeatmapScope = scopeKey
return response.results
},
},
],
rawDurationHistogram: [
[] as DurationHistogramRow[],
{
fetchDurationHistogram: async () => {
const controller = new AbortController()
actions.cancelInProgressDurationHistogram(controller)
const response = await api.tracing.durationHistogram(
{
dateRange: values.utcDateRange,
serviceNames:
values.filters.serviceNames.length > 0 ? values.filters.serviceNames : undefined,
filterGroup: values.queryFilterGroup as PropertyGroupFilter,
// Match the population the list shows (and the sparkline counts):
// root spans in Traces view, every matching span in Spans view.
rootSpans: values.filters.viewMode === 'traces',
},
controller.signal
)
actions.setDurationHistogramAbortController(null)
return response.results
},
},
],
})),
selectors({
sparklineData: [
(s) => [s.rawSparklineData],
(rows: SparklineRow[]): TracingSparklineData => {
if (!rows.length) {
return { data: [], dates: [], labels: [] }
}
let lastTime = ''
let i = -1
const labels: string[] = []
const dates: string[] = []
const accumulated = rows.reduce(
(accumulator, currentItem) => {
if (currentItem.time !== lastTime) {
labels.push(
humanFriendlyDetailedTime(currentItem.time, 'YYYY-MM-DD', 'HH:mm:ss', {
timestampStyle: 'absolute',
})
)
dates.push(currentItem.time)
lastTime = currentItem.time
i++
}
const key = currentItem.service
if (!key) {
return accumulator
}
if (!accumulator[key]) {
accumulator[key] = []
}
while (accumulator[key].length <= i) {
accumulator[key].push(0)
}
accumulator[key][i] += currentItem.count
return accumulator
},
{} as Record<string, number[]>
)
const data = Object.entries(accumulated)
.sort(([a], [b]) => a.localeCompare(b))
.map(([name, values], index) => ({
name,
values: values as number[],
color: dataColorVars[index % dataColorVars.length],
}))
.filter((series) => series.values.reduce((a, b) => a + b) > 0)
return { data, labels, dates }
},
],
// Total matching the filters, in the unit the list is showing: distinct traces in 'traces'
// mode, individual spans in 'spans' mode. Fed by the count endpoint, which returns both.
totalMatchingFilters: [
(s) => [s.matchingCounts, s.filters],
(matchingCounts: { count: number; traceCount: number }, filters: TracingFilters): number =>
filters.viewMode === 'spans' ? matchingCounts.count : matchingCounts.traceCount,
],
durationHistogramData: [
(s) => [s.rawDurationHistogram],
(rows: DurationHistogramRow[]): TracingDurationHistogramData => pivotDurationHistogram(rows, dataColorVars),
],
latencyHeatmapData: [
(s) => [s.rawLatencyHeatmap],
(rows: LatencyHeatmapRow[]): TracingLatencyHeatmapData => pivotLatencyHeatmap(rows),
],
// Single owner of the "show the latency heatmap?" rule — gates the fetch, the rendering,
// and the duration-histogram skip together. The feature-flag check here means a
// ?chart=heatmap deep link with the flag off degrades to the activity chart without
// fetching. The comparison overlay is a time-window drag surface, so the heatmap also
// yields to the activity chart while comparing.
showHeatmap: [
(s) => [s.filters, s.compareActive, s.featureFlags],
(filters: TracingFilters, compareActive: boolean, featureFlags: FeatureFlagsSet): boolean =>
!!featureFlags[FEATURE_FLAGS.TRACING_LATENCY_HEATMAP] &&
filters.chartType === 'heatmap' &&
!compareActive,
],
// The rows the list renders. 'traces' mode shows root spans only (one row per trace);
// 'spans' mode shows every matching span (root and child) flat. The fetch passes flatSpans
// to match, so in 'spans' mode the loaded spans are already the flat set.
listRows: [
(s) => [s.spans, s.filters],
(spans: Span[], filters: TracingFilters): Span[] => {
return filters.viewMode === 'spans' ? spans : spans.filter((s) => s.is_root_span)
},
],
// Memoized separately so visibleRowDurationRange (recomputed on every scroll tick) doesn't
// re-allocate the durations array — this only changes when the loaded rows do.
listRowDurations: [
(s) => [s.listRows],
(listRows: Span[]): number[] => listRows.map((span) => span.duration_nano),
],
// Single owner of the "show the duration histogram?" rule — the fetch decision, the
// highlight selector, and the scene's rendering all derive from this one place.
// An active comparison replaces the span list with the aggregate table, so the histogram
// (which mirrors the duration-sorted list) only applies outside it.
isDurationMode: [
(s) => [s.filters, s.compareActive],
(filters: TracingFilters, compareActive: boolean): boolean =>
filters.orderBy === 'duration' && !compareActive,
],
// Duration range covered by the currently-visible rows — the duration-space sibling of
// visibleRowDateRange below. When sorted by duration the visible rows are contiguous in
// duration space, so the histogram can sweep a highlight across the distribution as the
// user scrolls (the same interaction the time sparkline has under timestamp sort).
visibleRowDurationRange: [
(s) => [s.visibleRowRange, s.listRowDurations, s.isDurationMode],
(
visibleRowRange: VisibleRowRange | null,
listRowDurations: number[],
isDurationMode: boolean
): VisibleDurationRange | null => {
if (!isDurationMode) {
return null
}
return visibleDurationRange(visibleRowRange, listRowDurations)
},
],
// Date range covered by the currently-visible (scrolled-into-view) rows, so the sparkline can
// highlight the window the list is showing (mirrors the logs viewer). Only meaningful when the
// list is time-ordered — under duration (or any non-timestamp) sort, consecutive rows aren't
// contiguous in time, so the highlight would be meaningless. Suppress it then; the duration
// histogram (visibleRowDurationRange above) covers the duration-sorted case.
visibleRowDateRange: [
(s) => [s.visibleRowRange, s.listRows, s.orderBy],
(
visibleRowRange: VisibleRowRange | null,
listRows: Span[],
orderBy: TracingOrderBy
): VisibleSpanTimeRange | null => {
if (orderBy !== 'timestamp' || !visibleRowRange || listRows.length === 0) {
return null
}
const startIndex = Math.max(0, Math.min(visibleRowRange.startIndex, listRows.length - 1))
const stopIndex = Math.max(0, Math.min(visibleRowRange.stopIndex, listRows.length - 1))
const a = listRows[startIndex]?.timestamp
const b = listRows[stopIndex]?.timestamp
if (!a || !b) {
return null
}
const ta = dayjs(a)
const tb = dayjs(b)
const [earlier, later] = ta.isBefore(tb) ? [ta, tb] : [tb, ta]
return {
date_from: earlier.toISOString(),
date_to: later.toISOString(),
}
},
],
}),
listeners(({ actions, values }) => ({
handleFilterChange: ({ filterType, extraProps }) => {
insights.capture('tracing filter changed', { filter_type: filterType, ...extraProps })
actions.runQuery()
},
setDateRange: () => actions.handleFilterChange('date_range'),
setServiceNames: () => actions.handleFilterChange('service_names'),
setFilterGroup: () => actions.handleFilterChange('filter_group'),
setSort: ({ orderBy, orderDirection }) =>
actions.handleFilterChange('sort', { column: orderBy, direction: orderDirection }),
setViewMode: ({ viewMode }) => actions.handleFilterChange('view_mode', { mode: viewMode }),
setChartType: ({ chartType }) => actions.handleFilterChange('chart_type', { chart_type: chartType }),
setComparison: ({ comparison }) =>
actions.handleFilterChange('comparison', {
enabled: comparison !== null,
mode: comparison?.mode ?? null,
preset: comparison?.mode === 'time' ? comparison.preset : null,
}),
// Bulk restores (URL params, saved views) re-query without an interaction capture.
setFilters: () => actions.runQuery(),
applyHeatmapBrush: ({ selection }) => {
// While a refetch is in flight the rendered heatmap still shows the previous query's
// buckets. Ignore brushes until it settles so the selection maps against the buckets
// actually on screen rather than a stale grid.
if (values.latencyHeatmapLoading) {
return
}
const updates = heatmapBrushToFilters(
values.latencyHeatmapData,
selection,
values.filters.filterGroup,
values.utcDateRange.date_to
)
if (!updates) {
return
}
insights.capture('tracing filter changed', {
filter_type: 'heatmap_brush',
with_duration: !!updates.filterGroup,
})
// One dispatch → one re-query via the setFilters listener (and one URL write in the
// scene logic) — separate setDateRange + setFilterGroup would each run the query.
actions.setFilters(updates)
},
// Feature flags land asynchronously (insights.js /flags), potentially after the first
// runQuery. If that flip just made the heatmap visible (?chart=heatmap deep link with
// the flag on), fetch the data the earlier runQuery skipped.
setFeatureFlags: () => {
if (values.showHeatmap && values.rawLatencyHeatmap.length === 0 && !values.latencyHeatmapLoading) {
actions.fetchLatencyHeatmap()
}
},
// Overlay drags only refetch the aggregation — the sparkline canvas range stays fixed
// while the user moves windows around within it. The compare-flame refetch (viewer UI
// state) lives in tracingViewerLogic.
updateComparisonWindows: () => actions.fetchAggregation(),
runQuery: () => {
actions.clearSpans()
// The time sparkline is always fetched — it keeps the chart warm when the user flips back
// to timestamp sort. Duration sort additionally fetches the histogram that replaces it
// visually. The count endpoint feeds the "N traces/spans matching filters" label.
actions.fetchSparkline()
actions.fetchMatchingCounts()
if (values.showHeatmap) {
actions.fetchLatencyHeatmap()
}
// The heatmap wins the chart slot, so skip the histogram fetch it would hide.
if (values.isDurationMode && !values.showHeatmap) {
actions.fetchDurationHistogram()
}
if (values.compareActive) {
actions.fetchAggregation()
} else {
actions.fetchSpans()
}
},
cancelInProgressSpans: ({ controller }) => {
if (values.spansAbortController !== null) {
values.spansAbortController.abort(NEW_QUERY_STARTED_ERROR_MESSAGE)
}
actions.setSpansAbortController(controller)
},
cancelInProgressSparkline: ({ controller }) => {
if (values.sparklineAbortController !== null) {
values.sparklineAbortController.abort(NEW_QUERY_STARTED_ERROR_MESSAGE)
}
actions.setSparklineAbortController(controller)
},
cancelInProgressMatchingCounts: ({ controller }) => {
if (values.matchingCountsAbortController !== null) {
values.matchingCountsAbortController.abort(NEW_QUERY_STARTED_ERROR_MESSAGE)
}
actions.setMatchingCountsAbortController(controller)
},
cancelInProgressDurationHistogram: ({ controller }) => {
if (values.durationHistogramAbortController !== null) {
values.durationHistogramAbortController.abort(NEW_QUERY_STARTED_ERROR_MESSAGE)
}
actions.setDurationHistogramAbortController(controller)
},
cancelInProgressLatencyHeatmap: ({ controller }) => {
if (values.latencyHeatmapAbortController !== null) {
values.latencyHeatmapAbortController.abort(NEW_QUERY_STARTED_ERROR_MESSAGE)
}
actions.setLatencyHeatmapAbortController(controller)
},
cancelInProgressAggregation: ({ controller }) => {
if (values.aggregationAbortController !== null) {
values.aggregationAbortController.abort(NEW_QUERY_STARTED_ERROR_MESSAGE)
}
actions.setAggregationAbortController(controller)
},
cancelInProgressSpanTree: ({ controller }) => {
if (values.spanTreeAbortController !== null) {
values.spanTreeAbortController.abort(NEW_QUERY_STARTED_ERROR_MESSAGE)
}
actions.setSpanTreeAbortController(controller)
},
fetchSpansSuccess: () => {
captureTracingResults(values.listRows.length, 'spans')
},
fetchAggregationSuccess: ({ aggregation }) => {
captureTracingResults(aggregation.current.length, 'aggregation')
},
fetchSpansFailure: ({ error }) => {
if (!isUserInitiatedError(error)) {
toast.error(`Failed to load traces: ${error}`)
insights.capture('tracing query failed', { query_type: 'spans', error_message: String(error) })
}
},
fetchMatchingCountsFailure: ({ error }) => {
if (!isUserInitiatedError(error)) {
toast.error(`Failed to load the matching count: ${error}`)
}
},
fetchAggregationFailure: ({ error }) => {
if (!isUserInitiatedError(error)) {
toast.error(`Failed to load span aggregation: ${error}`)
}
},
fetchSpanTreeFailure: ({ error }) => {
if (!isUserInitiatedError(error)) {
toast.error(`Failed to load call tree: ${error}`)
}
},
loadMoreTraceSpansFailure: ({ error }) => {
if (!isUserInitiatedError(error)) {
toast.error(`Failed to load more spans: ${error}`)
}
},
// Surface a real fetch failure instead of letting the chart render an empty grid that
// reads as "No results matching filters" — an unreachable endpoint is not an empty query.
fetchLatencyHeatmapFailure: ({ error }) => {
if (!isUserInitiatedError(error)) {
toast.error(`Failed to load the latency heatmap: ${error}`)
}
},
})),
events(({ values }) => ({
beforeUnmount: () => {
if (values.spansAbortController) {
values.spansAbortController.abort('unmounting component')
}
if (values.sparklineAbortController) {
values.sparklineAbortController.abort('unmounting component')
}
if (values.matchingCountsAbortController) {
values.matchingCountsAbortController.abort('unmounting component')
}
if (values.durationHistogramAbortController) {
values.durationHistogramAbortController.abort('unmounting component')
}
if (values.latencyHeatmapAbortController) {
values.latencyHeatmapAbortController.abort('unmounting component')
}
if (values.spanTreeAbortController) {
values.spanTreeAbortController.abort('unmounting component')
}
if (values.aggregationAbortController) {
values.aggregationAbortController.abort('unmounting component')
}
},
})),
afterMount(({ actions, props: logicProps, values }) => {
if (logicProps.autoLoad && !values.hasRunQuery) {
actions.runQuery()
}
}),
])