Compare commits

..

6 Commits

Author SHA1 Message Date
Nikhil Mantri
c77b5b80fd Merge branch 'main' into fix/remove_decoding_url 2026-02-24 20:51:10 +05:30
Vinicius Lourenço
cb1a2a8a13 perf(bundle-size): lazy load pages to reduce main bundle size (#10230)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-02-24 10:41:40 +00:00
Nikhil Soni
1a5d37b25a fix: add missing filtering for ip address for scalar data (#10264)
* fix: add missing filtering for ip address for scalar data

In domain listing api for external api monitoring,
we have option to filter out the IP address but
it only handles timeseries and raw type data while
domain list handler returns scalar data.

* fix: switch to new derived attributes for ip filtering

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2026-02-24 10:26:10 +00:00
nikhilmantri0902
43509681fa chore: remove comments 2026-02-22 17:35:49 +05:30
nikhilmantri0902
ff5fcc0e98 chore: remove explicit URL decoding that causes crashes on K8s parameters 2026-02-21 11:21:37 +05:30
nikhilmantri0902
122d88c4d2 chore: fixed all failing sites where double decoding is happening in infra monitoring 2026-02-21 11:07:50 +05:30
32 changed files with 130 additions and 6460 deletions

View File

@@ -308,3 +308,15 @@ export const PublicDashboardPage = Loadable(
/* webpackChunkName: "Public Dashboard Page" */ 'pages/PublicDashboard'
),
);
export const AlertTypeSelectionPage = Loadable(
() =>
import(
/* webpackChunkName: "Alert Type Selection Page" */ 'pages/AlertTypeSelection'
),
);
export const MeterExplorerPage = Loadable(
() =>
import(/* webpackChunkName: "Meter Explorer Page" */ 'pages/MeterExplorer'),
);

View File

@@ -1,12 +1,10 @@
import { RouteProps } from 'react-router-dom';
import ROUTES from 'constants/routes';
import AlertTypeSelectionPage from 'pages/AlertTypeSelection';
import MessagingQueues from 'pages/MessagingQueues';
import MeterExplorer from 'pages/MeterExplorer';
import {
AlertHistory,
AlertOverview,
AlertTypeSelectionPage,
AllAlertChannels,
AllErrors,
ApiMonitoring,
@@ -29,6 +27,8 @@ import {
LogsExplorer,
LogsIndexToFields,
LogsSaveViews,
MessagingQueuesMainPage,
MeterExplorerPage,
MetricsExplorer,
OldLogsExplorer,
Onboarding,
@@ -399,28 +399,28 @@ const routes: AppRoutes[] = [
{
path: ROUTES.MESSAGING_QUEUES_KAFKA,
exact: true,
component: MessagingQueues,
component: MessagingQueuesMainPage,
key: 'MESSAGING_QUEUES_KAFKA',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_CELERY_TASK,
exact: true,
component: MessagingQueues,
component: MessagingQueuesMainPage,
key: 'MESSAGING_QUEUES_CELERY_TASK',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_OVERVIEW,
exact: true,
component: MessagingQueues,
component: MessagingQueuesMainPage,
key: 'MESSAGING_QUEUES_OVERVIEW',
isPrivate: true,
},
{
path: ROUTES.MESSAGING_QUEUES_KAFKA_DETAIL,
exact: true,
component: MessagingQueues,
component: MessagingQueuesMainPage,
key: 'MESSAGING_QUEUES_KAFKA_DETAIL',
isPrivate: true,
},
@@ -463,21 +463,21 @@ const routes: AppRoutes[] = [
{
path: ROUTES.METER,
exact: true,
component: MeterExplorer,
component: MeterExplorerPage,
key: 'METER',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER,
exact: true,
component: MeterExplorer,
component: MeterExplorerPage,
key: 'METER_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER_VIEWS,
exact: true,
component: MeterExplorer,
component: MeterExplorerPage,
key: 'METER_EXPLORER_VIEWS',
isPrivate: true,
},

View File

@@ -14,16 +14,10 @@ interface ITimelineV2Props {
startTimestamp: number;
endTimestamp: number;
timelineHeight: number;
offsetTimestamp: number;
}
function TimelineV2(props: ITimelineV2Props): JSX.Element {
const {
startTimestamp,
endTimestamp,
timelineHeight,
offsetTimestamp,
} = props;
const { startTimestamp, endTimestamp, timelineHeight } = props;
const [intervals, setIntervals] = useState<Interval[]>([]);
const [ref, { width }] = useMeasure<HTMLDivElement>();
const isDarkMode = useIsDarkMode();
@@ -36,10 +30,8 @@ function TimelineV2(props: ITimelineV2Props): JSX.Element {
const minIntervals = getMinimumIntervalsBasedOnWidth(width);
const intervalisedSpread = (spread / minIntervals) * 1.0;
const intervals = getIntervals(intervalisedSpread, spread, offsetTimestamp);
setIntervals(intervals);
}, [startTimestamp, endTimestamp, width, offsetTimestamp]);
setIntervals(getIntervals(intervalisedSpread, spread));
}, [startTimestamp, endTimestamp, width]);
if (endTimestamp < startTimestamp) {
console.error(

View File

@@ -64,71 +64,6 @@ export const resolveTimeFromInterval = (
export function getIntervals(
intervalSpread: number,
baseSpread: number,
offsetTimestamp: number, // ms offset from trace start (e.g. viewStart - traceStart)
): Interval[] {
const integerPartString = intervalSpread.toString().split('.')[0];
const integerPartLength = integerPartString.length;
const intervalSpreadNormalized =
intervalSpread < 1.0
? intervalSpread
: Math.floor(Number(integerPartString) / 10 ** (integerPartLength - 1)) *
10 ** (integerPartLength - 1);
let intervalUnit = INTERVAL_UNITS[0];
for (let idx = INTERVAL_UNITS.length - 1; idx >= 0; idx -= 1) {
const standardInterval = INTERVAL_UNITS[idx];
if (intervalSpread * standardInterval.multiplier >= 1) {
intervalUnit = INTERVAL_UNITS[idx];
break;
}
}
const intervals: Interval[] = [
{
// ✅ start label should reflect window start offset (relative to trace start)
label: `${toFixed(
resolveTimeFromInterval(offsetTimestamp, intervalUnit),
2,
)}${intervalUnit.name}`,
percentage: 0,
},
];
let tempBaseSpread = baseSpread;
let elapsedIntervals = 0;
while (tempBaseSpread && intervals.length < 20) {
let intervalTime: number;
if (tempBaseSpread <= 1.5 * intervalSpreadNormalized) {
intervalTime = elapsedIntervals + tempBaseSpread;
tempBaseSpread = 0;
} else {
intervalTime = elapsedIntervals + intervalSpreadNormalized;
tempBaseSpread -= intervalSpreadNormalized;
}
elapsedIntervals = intervalTime;
// ✅ label time = window offset + elapsed time inside window
const labelTime = offsetTimestamp + intervalTime;
intervals.push({
label: `${toFixed(resolveTimeFromInterval(labelTime, intervalUnit), 2)}${
intervalUnit.name
}`,
percentage: (intervalTime / baseSpread) * 100,
});
}
return intervals;
}
export function getIntervalsOld(
intervalSpread: number,
baseSpread: number,
offsetTimestamp: number,
): Interval[] {
const integerPartString = intervalSpread.toString().split('.')[0];
const integerPartLength = integerPartString.length;
@@ -171,10 +106,9 @@ export function getIntervalsOld(
}
elapsedIntervals = intervalTime;
const interval: Interval = {
label: `${toFixed(
resolveTimeFromInterval(intervalTime + offsetTimestamp, intervalUnit),
2,
)}${intervalUnit.name}`,
label: `${toFixed(resolveTimeFromInterval(intervalTime, intervalUnit), 2)}${
intervalUnit.name
}`,
percentage: (intervalTime / baseSpread) * 100,
};
intervals.push(interval);

View File

@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -103,9 +103,10 @@ function K8sClustersList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -105,9 +105,10 @@ function K8sDaemonSetsList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -106,9 +106,10 @@ function K8sDeploymentsList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -101,9 +101,10 @@ function K8sJobsList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -10,6 +10,7 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { safeParseJSON } from './commonUtils';
import { INFRA_MONITORING_K8S_PARAMS_KEYS, K8sCategory } from './constants';
import K8sFiltersSidePanel from './K8sFiltersSidePanel/K8sFiltersSidePanel';
import { IEntityColumn } from './utils';
@@ -58,9 +59,10 @@ function K8sHeader({
const urlFilters = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS);
let { filters } = currentQuery.builder.queryData[0];
if (urlFilters) {
const decoded = decodeURIComponent(urlFilters);
const parsed = JSON.parse(decoded);
filters = parsed;
const parsed = safeParseJSON<IBuilderQuery['filters']>(urlFilters);
if (parsed) {
filters = parsed;
}
}
return {
...currentQuery,

View File

@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -104,9 +104,10 @@ function K8sNamespacesList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -27,7 +27,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -99,9 +99,10 @@ function K8sNodesList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -29,7 +29,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -92,9 +92,10 @@ function K8sPodsList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -105,9 +105,10 @@ function K8sStatefulSetsList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -28,7 +28,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { getOrderByFromParams } from '../commonUtils';
import { getOrderByFromParams, safeParseJSON } from '../commonUtils';
import {
GetK8sEntityToAggregateAttribute,
INFRA_MONITORING_K8S_PARAMS_KEYS,
@@ -105,9 +105,10 @@ function K8sVolumesList({
const [groupBy, setGroupBy] = useState<IBuilderQuery['groupBy']>(() => {
const groupBy = searchParams.get(INFRA_MONITORING_K8S_PARAMS_KEYS.GROUP_BY);
if (groupBy) {
const decoded = decodeURIComponent(groupBy);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['groupBy'];
const parsed = safeParseJSON<IBuilderQuery['groupBy']>(groupBy);
if (parsed) {
return parsed;
}
}
return [];
});

View File

@@ -5,6 +5,7 @@
/* eslint-disable prefer-destructuring */
import { useMemo } from 'react';
import * as Sentry from '@sentry/react';
import { Color } from '@signozhq/design-tokens';
import { Table, Tooltip, Typography } from 'antd';
import { Progress } from 'antd/lib';
@@ -260,6 +261,19 @@ export const filterDuplicateFilters = (
return uniqueFilters;
};
export const safeParseJSON = <T,>(value: string): T | null => {
if (!value) {
return null;
}
try {
return JSON.parse(value) as T;
} catch (e) {
console.error('Error parsing JSON from URL parameter:', e);
// TODO: Should we capture this error in Sentry?
return null;
}
};
export const getOrderByFromParams = (
searchParams: URLSearchParams,
returnNullAsDefault = false,
@@ -271,9 +285,12 @@ export const getOrderByFromParams = (
INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
);
if (orderByFromParams) {
const decoded = decodeURIComponent(orderByFromParams);
const parsed = JSON.parse(decoded);
return parsed as { columnName: string; order: 'asc' | 'desc' };
const parsed = safeParseJSON<{ columnName: string; order: 'asc' | 'desc' }>(
orderByFromParams,
);
if (parsed) {
return parsed;
}
}
if (returnNullAsDefault) {
return null;
@@ -287,13 +304,7 @@ export const getFiltersFromParams = (
): IBuilderQuery['filters'] | null => {
const filtersFromParams = searchParams.get(queryKey);
if (filtersFromParams) {
try {
const decoded = decodeURIComponent(filtersFromParams);
const parsed = JSON.parse(decoded);
return parsed as IBuilderQuery['filters'];
} catch (error) {
return null;
}
return safeParseJSON<IBuilderQuery['filters']>(filtersFromParams);
}
return null;
};

View File

@@ -1,88 +0,0 @@
# Flamegraph Canvas POC Notes
## Overview
This document tracks the proof-of-concept (POC) implementation of a canvas-based flamegraph rendering system, replacing the previous DOM-based approach.
## Implementation Status
### ✅ Completed Features
1. **Canvas-based Rendering**
- Replaced DOM elements (`react-virtuoso` and `div.span-item`) with a single HTML5 Canvas
- Implemented `drawFlamegraph` function to render all spans as rectangles on canvas
- Added device pixel ratio (DPR) support for crisp rendering
2. **Time-window-based Zoom**
- Replaced pixel-based zoom/pan with time-window-based approach (`viewStartTs`, `viewEndTs`)
- Prevents pixelation by redrawing from data with new time bounds
- Zoom anchors to cursor position
- Horizontal zoom works correctly (min: 1/100th of trace, max: full trace)
3. **Drag to Pan**
- Implemented drag-to-pan functionality for navigating the canvas
- Differentiates between click (span selection) and drag (panning) based on distance moved
- Prevents unwanted window zoom
4. **Minimap with 2D Navigation**
- Canvas-based minimap showing density histogram (time × levels)
- 2D brush overlay for both horizontal (time) and vertical (levels) navigation
- Draggable brush to pan both dimensions
- Bidirectional synchronization between main canvas and minimap
5. **Timeline Synchronization**
- `TimelineV2` component synchronized with visible time window
- Updates correctly during zoom and pan operations
6. **Hit Testing**
- Implemented span rectangle tracking for click detection
- Tooltip on hover
- Span selection via click
### ❌ Known Issues / Not Working
1. **Vertical Zoom - NOT WORKING**
- **Status**: Attempted implementation but not functioning correctly
- **Issue**: When horizontal zoom reaches maximum (full trace width), vertical zoom cannot continue to zoom out further
- **Attempted Solution**: Added `rowHeightScale` state to control vertical row spacing, but the implementation does not work as expected
- **Impact**: Users cannot fully zoom out vertically to see all levels when horizontal zoom is at maximum
- **Next Steps**: Needs further investigation and alternative approach
2. **Timeline Scale Alignment - NOT WORKING PROPERLY**
- **Status**: Issue identified but not fully resolved
- **Issue**: The timeline scale does not align properly when dragging/panning the canvas. The timeline aligns correctly during zoom operations, but not during drag/pan operations.
- **Impact**: Timeline may show incorrect time values while dragging the canvas
- **Attempted Solution**: Used refs (`viewStartTsRef`, `viewEndTsRef`) to track current time window and incremental delta calculation, but issue persists
- **Next Steps**: Needs further investigation to ensure timeline stays synchronized during all interaction types
### 🔄 Pending / Future Work
1. **Performance Optimization**
- Consider adding an interaction layer (separate canvas on top) for better performance
- Optimize rendering for large traces
2. **Code Quality**
- Reduce cognitive complexity of `drawFlamegraph` function (currently 26, target: 15)
- Reduce cognitive complexity of `drawMinimap` function (currently 30, target: 15)
3. **Additional Features**
- Keyboard shortcuts for navigation
- Better zoom controls
- Export functionality
## Technical Details
### Key Files Modified
- `frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.tsx` - Main rendering component
- `frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.styles.scss` - Styles for canvas and minimap
- `frontend/src/components/TimelineV2/TimelineV2.tsx` - Timeline synchronization
### Key Concepts
- **Time-window-based zoom**: Instead of scaling canvas bitmap, redraw from data with new time bounds
- **Device Pixel Ratio**: Render at DPR resolution for crisp display on high-DPI screens
- **2D Minimap**: Shows density heatmap across both time (horizontal) and levels (vertical) dimensions
- **Brush Navigation**: Draggable rectangle overlay for panning both dimensions
## Notes
- This is a POC implementation - code quality and optimization can be improved after validation
- Some linting warnings (cognitive complexity) are acceptable for POC phase
- All changes should be validated before production use

View File

@@ -1,5 +1,3 @@
// @ts-nocheck
/* eslint-disable */
import { useEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Progress, Skeleton, Tooltip, Typography } from 'antd';
@@ -15,13 +13,8 @@ import { Span } from 'types/api/trace/getTraceV2';
import { TraceFlamegraphStates } from './constants';
import Error from './TraceFlamegraphStates/Error/Error';
import NoData from './TraceFlamegraphStates/NoData/NoData';
// import Success from './TraceFlamegraphStates/Success/SuccessV2';
// import Success from './TraceFlamegraphStates/Success/SuccessV3_without_minimap_best';
import Success from './TraceFlamegraphStates/Success/Success_zoom';
import Success from './TraceFlamegraphStates/Success/Success';
// import Success from './TraceFlamegraphStates/Success/Success_zoom_api';
// import Success from './TraceFlamegraphStates/Success/SuccessCursor';
// import Success from './TraceFlamegraphStates/Success/Success';
import './PaginatedTraceFlamegraph.styles.scss';
interface ITraceFlamegraphProps {
@@ -45,6 +38,7 @@ function TraceFlamegraph(props: ITraceFlamegraphProps): JSX.Element {
const [firstSpanAtFetchLevel, setFirstSpanAtFetchLevel] = useState<string>(
urlQuery.get('spanId') || '',
);
useEffect(() => {
setFirstSpanAtFetchLevel(urlQuery.get('spanId') || '');
}, [urlQuery]);
@@ -52,9 +46,6 @@ function TraceFlamegraph(props: ITraceFlamegraphProps): JSX.Element {
const { data, isFetching, error } = useGetTraceFlamegraph({
traceId,
selectedSpanId: firstSpanAtFetchLevel,
limit: 100001,
// boundaryStartTsMilli: 0,
// boundarEndTsMilli: 10000,
});
// get the current state of trace flamegraph based on the API lifecycle

View File

@@ -3,11 +3,6 @@
overflow-x: hidden;
overflow-y: auto;
&.trace-flamegraph-canvas {
overflow: hidden;
position: relative;
}
.trace-flamegraph-virtuoso {
overflow-x: hidden;

View File

@@ -1,6 +1,3 @@
// @ts-nocheck
/* eslint-disable */
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import {
@@ -26,12 +23,6 @@ import { toFixed } from 'utils/toFixed';
import './Success.styles.scss';
// Constants for rendering
const ROW_HEIGHT = 24; // 18px height + 6px padding
const SPAN_BAR_HEIGHT = 12;
const EVENT_DOT_SIZE = 6;
const SPAN_BAR_Y_OFFSET = 3; //
interface ITraceMetadata {
startTime: number;
endTime: number;
@@ -44,14 +35,6 @@ interface ISuccessProps {
traceMetadata: ITraceMetadata;
selectedSpan: Span | undefined;
}
interface SpanRect {
span: FlamegraphSpan;
x: number;
y: number;
width: number;
height: number;
level: number;
}
function Success(props: ISuccessProps): JSX.Element {
const {
@@ -65,28 +48,6 @@ function Success(props: ISuccessProps): JSX.Element {
const history = useHistory();
const isDarkMode = useIsDarkMode();
const virtuosoRef = useRef<VirtuosoHandle>(null);
const baseCanvasRef = useRef<HTMLCanvasElement>(null);
const interactionCanvasRef = useRef<HTMLCanvasElement>(null);
const minimapRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
console.log('spans.length', spans.length);
// Calculate total canvas height. this is coming less
const totalHeight = spans.length * ROW_HEIGHT;
// Build a flat array of span rectangles for hit testing.
// consider per level buckets to improve hit testing
const spanRects = useRef<SpanRect[]>([]);
// Time window state (instead of zoom/pan in pixel space)
const [viewStartTs, setViewStartTs] = useState<number>(
traceMetadata.startTime,
);
const [viewEndTs, setViewEndTs] = useState<number>(traceMetadata.endTime);
const [scrollTop, setScrollTop] = useState<number>(0);
const [hoveredSpanId, setHoveredSpanId] = useState<string>('');
const renderSpanLevel = useCallback(
(_: number, spans: FlamegraphSpan[]): JSX.Element => (
@@ -196,305 +157,16 @@ function Success(props: ISuccessProps): JSX.Element {
});
}, [firstSpanAtFetchLevel, spans]);
// Draw a single event dot
const drawEventDot = useCallback(
(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
isError: boolean,
): void => {
// could be optimized:
// ctx.beginPath();
// ctx.moveTo(x, y - size/2);
// ctx.lineTo(x + size/2, y);
// ctx.lineTo(x, y + size/2);
// ctx.lineTo(x - size/2, y);
// ctx.closePath();
// ctx.fill();
// ctx.stroke();
ctx.save();
ctx.translate(x, y);
ctx.rotate(Math.PI / 4); // 45 degrees
if (isError) {
ctx.fillStyle = isDarkMode ? 'rgb(239, 68, 68)' : 'rgb(220, 38, 38)';
ctx.strokeStyle = isDarkMode ? 'rgb(185, 28, 28)' : 'rgb(153, 27, 27)';
} else {
ctx.fillStyle = isDarkMode ? 'rgb(14, 165, 233)' : 'rgb(6, 182, 212)';
ctx.strokeStyle = isDarkMode ? 'rgb(2, 132, 199)' : 'rgb(8, 145, 178)';
}
ctx.lineWidth = 1;
ctx.fillRect(
-EVENT_DOT_SIZE / 2,
-EVENT_DOT_SIZE / 2,
EVENT_DOT_SIZE,
EVENT_DOT_SIZE,
);
ctx.strokeRect(
-EVENT_DOT_SIZE / 2,
-EVENT_DOT_SIZE / 2,
EVENT_DOT_SIZE,
EVENT_DOT_SIZE,
);
ctx.restore();
},
[isDarkMode],
);
// Get CSS color value from color string or CSS variable
// const getColorValue = useCallback((color: string): string => {
// // if (color.startsWith('var(')) {
// // // For CSS variables, we need to get computed value
// // const tempDiv = document.createElement('div');
// // tempDiv.style.color = color;
// // document.body.appendChild(tempDiv);
// // const computedColor = window.getComputedStyle(tempDiv).color;
// // document.body.removeChild(tempDiv);
// // return computedColor;
// // }
// return color;
// }, []);
// Get span color based on service, error state, and selection
// separate this when introducing interaction canvas
const getSpanColor = useCallback(
(span: FlamegraphSpan): string => {
let color = generateColor(span.serviceName, themeColors.traceDetailColors);
if (span.hasError) {
color = isDarkMode ? 'rgb(239, 68, 68)' : 'rgb(220, 38, 38)';
}
// else {
// color = getColorValue(color);
// }
// Apply selection/hover highlight
//hover/selection highlight in getSpanColor forces base redraw. clipping necessary.
if (selectedSpan?.spanId === span.spanId || hoveredSpanId === span.spanId) {
const colorObj = Color(color);
color = isDarkMode
? colorObj.lighten(0.7).hex()
: colorObj.darken(0.7).hex();
}
return color;
},
[isDarkMode, selectedSpan, hoveredSpanId],
);
// Draw a single span and its events
const drawSpan = useCallback(
(
ctx: CanvasRenderingContext2D,
span: FlamegraphSpan,
x: number,
y: number,
width: number,
levelIndex: number,
spanRectsArray: SpanRect[],
): void => {
const color = getSpanColor(span); // do not depend on hover/clicks
const spanY = y + SPAN_BAR_Y_OFFSET;
// Draw span rectangle
ctx.fillStyle = color;
ctx.beginPath();
// see if we can avoid roundRect as it is performance intensive
ctx.roundRect(x, spanY, width, SPAN_BAR_HEIGHT, 6);
ctx.fill();
// Store rect for hit testing
// consider per level buckets to improve hit testing
// So hover can:
// compute level from y
// search only within that row
spanRectsArray.push({
span,
x,
y: spanY,
width,
height: SPAN_BAR_HEIGHT,
level: levelIndex,
});
// Draw events
// think about optimizing this.
// if span is too small to draw events, skip drawing events???
span.event?.forEach((event) => {
const eventTimeMs = event.timeUnixNano / 1e6;
const eventOffsetPercent =
((eventTimeMs - span.timestamp) / (span.durationNano / 1e6)) * 100;
const clampedOffset = Math.max(1, Math.min(eventOffsetPercent, 99));
const eventX = x + (clampedOffset / 100) * width;
const eventY = spanY + SPAN_BAR_HEIGHT / 2;
// LOD guard: skip events if span too narrow
// if (width < EVENT_DOT_SIZE) {
// return;
// }
drawEventDot(ctx, eventX, eventY, event.isError);
});
},
[getSpanColor, drawEventDot],
);
const drawFlamegraph = useCallback(() => {
const canvas = baseCanvasRef.current;
const container = containerRef.current;
if (!canvas || !container) {
return;
}
const ctx = canvas.getContext('2d');
if (!ctx) {
return;
}
const dpr = window.devicePixelRatio || 1;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const timeSpan = viewEndTs - viewStartTs;
if (timeSpan <= 0) {
return;
}
const cssWidth = canvas.width / dpr;
// ---- Vertical clipping window ----
const viewportHeight = container.clientHeight;
const overscan = 4;
const firstLevel = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - overscan);
const visibleLevelCount =
Math.ceil(viewportHeight / ROW_HEIGHT) + 2 * overscan;
const lastLevel = Math.min(spans.length - 1, firstLevel + visibleLevelCount);
// ---- Clear only visible region (recommended) ----
const clearTop = firstLevel * ROW_HEIGHT;
const clearHeight = (lastLevel - firstLevel + 1) * ROW_HEIGHT;
ctx.clearRect(0, clearTop, cssWidth, clearHeight);
const spanRectsArray: SpanRect[] = [];
// ---- Draw only visible levels ----
for (let levelIndex = firstLevel; levelIndex <= lastLevel; levelIndex++) {
const levelSpans = spans[levelIndex];
if (!levelSpans) {
continue;
}
const y = levelIndex * ROW_HEIGHT;
for (let i = 0; i < levelSpans.length; i++) {
const span = levelSpans[i];
const spanStartMs = span.timestamp;
const spanEndMs = span.timestamp + span.durationNano / 1e6;
// Time culling (already correct)
if (spanEndMs < viewStartTs || spanStartMs > viewEndTs) {
continue;
}
const leftOffset = ((spanStartMs - viewStartTs) / timeSpan) * cssWidth;
const rightEdge = ((spanEndMs - viewStartTs) / timeSpan) * cssWidth;
let width = rightEdge - leftOffset;
// Clamp to visible x-range
if (leftOffset < 0) {
width += leftOffset;
if (width <= 0) {
continue;
}
}
if (rightEdge > cssWidth) {
width = cssWidth - Math.max(0, leftOffset);
if (width <= 0) {
continue;
}
}
// Optional: minimum 1px width
if (width > 0 && width < 1) {
width = 1;
}
drawSpan(
ctx,
span,
Math.max(0, leftOffset),
y,
width,
levelIndex,
spanRectsArray,
);
}
}
spanRects.current = spanRectsArray;
}, [spans, viewStartTs, viewEndTs, scrollTop, drawSpan]);
// Handle canvas resize with device pixel ratio
useEffect(() => {
const canvas = baseCanvasRef.current;
const container = containerRef.current;
if (!canvas || !container) {
return;
}
const updateCanvasSize = (): void => {
const rect = container.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
// Set CSS size
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${totalHeight}px`;
// Set actual pixel size (accounting for DPR)
// Only update if size actually changed to prevent unnecessary redraws
const newWidth = Math.floor(rect.width * dpr);
const newHeight = Math.floor(totalHeight * dpr);
if (canvas.width !== newWidth || canvas.height !== newHeight) {
canvas.width = newWidth;
canvas.height = newHeight;
// Redraw with current time window (preserves zoom/pan)
drawFlamegraph();
}
};
const resizeObserver = new ResizeObserver(updateCanvasSize);
resizeObserver.observe(container);
// Initial size
updateCanvasSize();
// Handle DPR changes (e.g., moving window between screens)
const handleDPRChange = (): void => {
updateCanvasSize();
};
window
.matchMedia('(resolution: 1dppx)')
.addEventListener('change', handleDPRChange);
return (): void => {
resizeObserver.disconnect();
window
.matchMedia('(resolution: 1dppx)')
.removeEventListener('change', handleDPRChange);
};
}, [drawFlamegraph, totalHeight]);
// Re-draw when data changes
useEffect(() => {
drawFlamegraph();
}, [drawFlamegraph]);
return (
<>
<div ref={containerRef} className="trace-flamegraph trace-flamegraph-canvas">
<canvas ref={baseCanvasRef}></canvas>
<div className="trace-flamegraph">
<Virtuoso
ref={virtuosoRef}
className="trace-flamegraph-virtuoso"
data={spans}
itemContent={renderSpanLevel}
rangeChanged={handleRangeChanged}
/>
</div>
<TimelineV2
startTimestamp={traceMetadata.startTime}

View File

@@ -1,890 +0,0 @@
// @ts-nocheck
/* eslint-disable */
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { Button } from 'antd';
import Color from 'color';
import TimelineV2 from 'components/TimelineV2/TimelineV2';
import { themeColors } from 'constants/theme';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import { FlamegraphSpan } from 'types/api/trace/getTraceFlamegraph';
import { Span } from 'types/api/trace/getTraceV2';
import './Success.styles.scss';
interface ITraceMetadata {
startTime: number;
endTime: number;
}
interface ISuccessProps {
spans: FlamegraphSpan[][];
firstSpanAtFetchLevel: string;
setFirstSpanAtFetchLevel: Dispatch<SetStateAction<string>>;
traceMetadata: ITraceMetadata;
selectedSpan: Span | undefined;
}
// Constants for rendering
const ROW_HEIGHT = 24; // 18px height + 6px padding
const SPAN_BAR_HEIGHT = 12;
const EVENT_DOT_SIZE = 6;
const SPAN_BAR_Y_OFFSET = 3; // Center the 12px bar in 18px row
interface SpanRect {
span: FlamegraphSpan;
x: number;
y: number;
width: number;
height: number;
level: number;
}
function Success(props: ISuccessProps): JSX.Element {
const {
spans,
setFirstSpanAtFetchLevel,
traceMetadata,
firstSpanAtFetchLevel,
selectedSpan,
} = props;
const { search } = useLocation();
const history = useHistory();
const isDarkMode = useIsDarkMode();
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [hoveredSpanId, setHoveredSpanId] = useState<string>('');
const [tooltipContent, setTooltipContent] = useState<{
content: string;
x: number;
y: number;
} | null>(null);
const [scrollTop, setScrollTop] = useState<number>(0);
// Time window state (instead of zoom/pan in pixel space)
const [viewStartTs, setViewStartTs] = useState<number>(
traceMetadata.startTime,
);
const [viewEndTs, setViewEndTs] = useState<number>(traceMetadata.endTime);
const [isSpacePressed, setIsSpacePressed] = useState<boolean>(false);
// Refs to avoid stale state during rapid wheel events and dragging
const viewStartRef = useRef(viewStartTs);
const viewEndRef = useRef(viewEndTs);
useEffect(() => {
viewStartRef.current = viewStartTs;
viewEndRef.current = viewEndTs;
}, [viewStartTs, viewEndTs]);
// Drag state in refs to avoid re-renders during drag
const isDraggingRef = useRef(false);
const dragStartRef = useRef<{ x: number; y: number } | null>(null);
const dragDistanceRef = useRef(0);
const suppressClickRef = useRef(false);
// Scroll ref to avoid recreating getCanvasPointer on every scroll
const scrollTopRef = useRef(0);
useEffect(() => {
scrollTopRef.current = scrollTop;
}, [scrollTop]);
// Build a flat array of span rectangles for hit testing
const spanRects = useRef<SpanRect[]>([]);
// Get span color based on service, error state, and selection
const getSpanColor = useCallback(
(span: FlamegraphSpan): string => {
let color = generateColor(span.serviceName, themeColors.traceDetailColors);
if (span.hasError) {
color = isDarkMode ? 'rgb(239, 68, 68)' : 'rgb(220, 38, 38)';
}
// Apply selection/hover highlight
if (selectedSpan?.spanId === span.spanId || hoveredSpanId === span.spanId) {
const colorObj = Color(color);
color = isDarkMode
? colorObj.lighten(0.7).hex()
: colorObj.darken(0.7).hex();
}
return color;
},
[isDarkMode, selectedSpan, hoveredSpanId],
);
// Draw a single event dot
const drawEventDot = useCallback(
(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
isError: boolean,
): void => {
// could be optimized:
// ctx.beginPath();
// ctx.moveTo(x, y - size/2);
// ctx.lineTo(x + size/2, y);
// ctx.lineTo(x, y + size/2);
// ctx.lineTo(x - size/2, y);
// ctx.closePath();
// ctx.fill();
// ctx.stroke();
ctx.save();
ctx.translate(x, y);
ctx.rotate(Math.PI / 4); // 45 degrees
if (isError) {
ctx.fillStyle = isDarkMode ? 'rgb(239, 68, 68)' : 'rgb(220, 38, 38)';
ctx.strokeStyle = isDarkMode ? 'rgb(185, 28, 28)' : 'rgb(153, 27, 27)';
} else {
ctx.fillStyle = isDarkMode ? 'rgb(14, 165, 233)' : 'rgb(6, 182, 212)';
ctx.strokeStyle = isDarkMode ? 'rgb(2, 132, 199)' : 'rgb(8, 145, 178)';
}
ctx.lineWidth = 1;
ctx.fillRect(
-EVENT_DOT_SIZE / 2,
-EVENT_DOT_SIZE / 2,
EVENT_DOT_SIZE,
EVENT_DOT_SIZE,
);
ctx.strokeRect(
-EVENT_DOT_SIZE / 2,
-EVENT_DOT_SIZE / 2,
EVENT_DOT_SIZE,
EVENT_DOT_SIZE,
);
ctx.restore();
},
[isDarkMode],
);
// Draw a single span and its events
const drawSpan = useCallback(
(
ctx: CanvasRenderingContext2D,
span: FlamegraphSpan,
x: number,
y: number,
width: number,
levelIndex: number,
spanRectsArray: SpanRect[],
): void => {
const color = getSpanColor(span); // do not depend on hover/clicks
const spanY = y + SPAN_BAR_Y_OFFSET;
// Draw span rectangle
ctx.fillStyle = color;
ctx.beginPath();
ctx.roundRect(x, spanY, width, SPAN_BAR_HEIGHT, 6);
ctx.fill();
// Store rect for hit testing
// consider per level buckets to improve hit testing
// So hover can:
// compute level from y
// search only within that row
spanRectsArray.push({
span,
x,
y: spanY,
width,
height: SPAN_BAR_HEIGHT,
level: levelIndex,
});
// Draw events
// think about optimizing this.
// if span is too small to draw events, skip drawing events???
span.event?.forEach((event) => {
const eventTimeMs = event.timeUnixNano / 1e6;
const eventOffsetPercent =
((eventTimeMs - span.timestamp) / (span.durationNano / 1e6)) * 100;
const clampedOffset = Math.max(1, Math.min(eventOffsetPercent, 99));
const eventX = x + (clampedOffset / 100) * width;
const eventY = spanY + SPAN_BAR_HEIGHT / 2;
drawEventDot(ctx, eventX, eventY, event.isError);
});
},
[getSpanColor, drawEventDot],
);
// Draw the flamegraph on canvas
const drawFlamegraph = useCallback(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) {
return;
}
const ctx = canvas.getContext('2d');
if (!ctx) {
return;
}
const dpr = window.devicePixelRatio || 1;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const timeSpan = viewEndTs - viewStartTs;
if (timeSpan <= 0) {
return;
}
const cssWidth = canvas.width / dpr;
// ---- Vertical clipping window ----
const viewportHeight = container.clientHeight;
const overscan = 4;
const firstLevel = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - overscan);
const visibleLevelCount =
Math.ceil(viewportHeight / ROW_HEIGHT) + 2 * overscan;
const lastLevel = Math.min(spans.length - 1, firstLevel + visibleLevelCount);
// ---- Clear only visible region (recommended) ----
const clearTop = firstLevel * ROW_HEIGHT;
const clearHeight = (lastLevel - firstLevel + 1) * ROW_HEIGHT;
ctx.clearRect(0, clearTop, cssWidth, clearHeight);
const spanRectsArray: SpanRect[] = [];
// ---- Draw only visible levels ----
for (let levelIndex = firstLevel; levelIndex <= lastLevel; levelIndex++) {
const levelSpans = spans[levelIndex];
if (!levelSpans) {
continue;
}
const y = levelIndex * ROW_HEIGHT;
for (let i = 0; i < levelSpans.length; i++) {
const span = levelSpans[i];
const spanStartMs = span.timestamp;
const spanEndMs = span.timestamp + span.durationNano / 1e6;
// Time culling (already correct)
if (spanEndMs < viewStartTs || spanStartMs > viewEndTs) {
continue;
}
const leftOffset = ((spanStartMs - viewStartTs) / timeSpan) * cssWidth;
const rightEdge = ((spanEndMs - viewStartTs) / timeSpan) * cssWidth;
let width = rightEdge - leftOffset;
// Clamp to visible x-range
if (leftOffset < 0) {
width += leftOffset;
if (width <= 0) {
continue;
}
}
if (rightEdge > cssWidth) {
width = cssWidth - Math.max(0, leftOffset);
if (width <= 0) {
continue;
}
}
// Optional: minimum 1px width
if (width > 0 && width < 1) {
width = 1;
}
drawSpan(
ctx,
span,
Math.max(0, leftOffset),
y,
width,
levelIndex,
spanRectsArray,
);
}
}
spanRects.current = spanRectsArray;
}, [spans, viewStartTs, viewEndTs, scrollTop, drawSpan]);
// Calculate total canvas height
const totalHeight = spans.length * ROW_HEIGHT;
console.log('time: ', {
start: traceMetadata.startTime,
end: traceMetadata.endTime,
});
// Initialize time window when trace metadata changes (only if not already set)
useEffect(() => {
// Only reset if we're at the default view (full trace)
// This prevents resetting zoom/pan when metadata updates
if (
viewStartTs === traceMetadata.startTime &&
viewEndTs === traceMetadata.endTime
) {
// Already at default, no need to update
return;
}
// Only reset if the trace bounds have actually changed significantly
const currentSpan = viewEndTs - viewStartTs;
const fullSpan = traceMetadata.endTime - traceMetadata.startTime;
// If we're zoomed in, preserve the zoom level relative to new bounds
if (currentSpan < fullSpan * 0.99) {
// We're zoomed in, adjust the window proportionally
const ratio = currentSpan / fullSpan;
const newSpan = (traceMetadata.endTime - traceMetadata.startTime) * ratio;
const center = (viewStartTs + viewEndTs) / 2;
const newStart = Math.max(
traceMetadata.startTime,
Math.min(center - newSpan / 2, traceMetadata.endTime - newSpan),
);
setViewStartTs(newStart);
setViewEndTs(newStart + newSpan);
} else {
// We're at full view, reset to new full view
setViewStartTs(traceMetadata.startTime);
setViewEndTs(traceMetadata.endTime);
}
}, [traceMetadata.startTime, traceMetadata.endTime, viewStartTs, viewEndTs]);
// Handle canvas resize with device pixel ratio
useEffect(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) {
return;
}
const updateCanvasSize = (): void => {
const rect = container.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
// Set CSS size
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${totalHeight}px`;
// Set actual pixel size (accounting for DPR)
// Only update if size actually changed to prevent unnecessary redraws
const newWidth = Math.floor(rect.width * dpr);
const newHeight = Math.floor(totalHeight * dpr);
if (canvas.width !== newWidth || canvas.height !== newHeight) {
canvas.width = newWidth;
canvas.height = newHeight;
// Redraw with current time window (preserves zoom/pan)
drawFlamegraph();
}
};
const resizeObserver = new ResizeObserver(updateCanvasSize);
resizeObserver.observe(container);
// Initial size
updateCanvasSize();
// Handle DPR changes (e.g., moving window between screens)
const handleDPRChange = (): void => {
updateCanvasSize();
};
window
.matchMedia('(resolution: 1dppx)')
.addEventListener('change', handleDPRChange);
return (): void => {
resizeObserver.disconnect();
window
.matchMedia('(resolution: 1dppx)')
.removeEventListener('change', handleDPRChange);
};
}, [drawFlamegraph, totalHeight]);
// Re-draw when data changes
useEffect(() => {
drawFlamegraph();
}, [drawFlamegraph]);
// Find span at given canvas coordinates
const findSpanAtPosition = useCallback((x: number, y: number):
| SpanRect
| undefined => {
return spanRects.current.find(
(spanRect) =>
x >= spanRect.x &&
x <= spanRect.x + spanRect.width &&
y >= spanRect.y &&
y <= spanRect.y + spanRect.height,
);
}, []);
// Utility to convert client coordinates to CSS canvas coordinates
const getCanvasPointer = useCallback((clientX: number, clientY: number): {
cssX: number;
cssY: number;
cssWidth: number;
} | null => {
const canvas = canvasRef.current;
if (!canvas) {
return null;
}
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
const cssWidth = canvas.width / dpr;
const cssX = (clientX - rect.left) * (cssWidth / rect.width);
const cssY = clientY - rect.top + scrollTopRef.current;
return { cssX, cssY, cssWidth };
}, []);
// Handle mouse move for hover and dragging
const handleMouseMove = useCallback(
(event: React.MouseEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
const rect = canvas.getBoundingClientRect();
console.log('event', { clientX: event.clientX, clientY: event.clientY });
// ---- Dragging (pan in time space) ----
if (isDraggingRef.current && dragStartRef.current) {
const deltaX = event.clientX - dragStartRef.current.x;
const deltaY = event.clientY - dragStartRef.current.y;
console.log('delta', { deltaY, deltaX });
const totalDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
dragDistanceRef.current = totalDistance;
const timeSpan = viewEndRef.current - viewStartRef.current;
const deltaTime = (deltaX / rect.width) * timeSpan;
const newStart = viewStartRef.current - deltaTime;
const clampedStart = Math.max(
traceMetadata.startTime,
Math.min(newStart, traceMetadata.endTime - timeSpan),
);
const clampedEnd = clampedStart + timeSpan;
setViewStartTs(clampedStart);
setViewEndTs(clampedEnd);
dragStartRef.current = {
x: event.clientX,
y: event.clientY,
};
return;
}
// ---- Hover ----
const pointer = getCanvasPointer(event.clientX, event.clientY);
if (!pointer) {
return;
}
const { cssX, cssY } = pointer;
const hoveredSpan = findSpanAtPosition(cssX, cssY);
if (hoveredSpan) {
setHoveredSpanId(hoveredSpan.span.spanId);
setTooltipContent({
content: hoveredSpan.span.name,
x: event.clientX,
y: event.clientY,
});
canvas.style.cursor = 'pointer';
} else {
setHoveredSpanId('');
setTooltipContent(null);
// Set cursor based on space key state when not hovering
canvas.style.cursor = isSpacePressed ? 'grab' : 'default';
}
},
[findSpanAtPosition, traceMetadata, getCanvasPointer, isSpacePressed],
);
// Handle key down for space key
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent): void => {
if (event.code === 'Space') {
event.preventDefault();
setIsSpacePressed(true);
}
};
const handleKeyUp = (event: KeyboardEvent): void => {
if (event.code === 'Space') {
event.preventDefault();
setIsSpacePressed(false);
}
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
return (): void => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
};
}, []);
const handleMouseDown = useCallback(
(event: React.MouseEvent<HTMLCanvasElement>) => {
if (event.button !== 0) {
return;
} // left click only
event.preventDefault();
isDraggingRef.current = true;
dragStartRef.current = {
x: event.clientX,
y: event.clientY,
};
dragDistanceRef.current = 0;
const canvas = canvasRef.current;
if (canvas) {
canvas.style.cursor = 'grabbing';
}
},
[],
);
const handleMouseUp = useCallback(() => {
const wasDrag = dragDistanceRef.current > 5;
suppressClickRef.current = wasDrag; // 👈 key fix: suppress click after drag
isDraggingRef.current = false;
dragStartRef.current = null;
dragDistanceRef.current = 0;
const canvas = canvasRef.current;
if (canvas) {
canvas.style.cursor = 'grab';
}
return wasDrag;
}, []);
const handleMouseLeave = useCallback(() => {
isOverFlamegraphRef.current = false;
setHoveredSpanId('');
setTooltipContent(null);
isDraggingRef.current = false;
dragStartRef.current = null;
dragDistanceRef.current = 0;
const canvas = canvasRef.current;
if (canvas) {
canvas.style.cursor = 'grab';
}
}, []);
const handleClick = useCallback(
(event: React.MouseEvent<HTMLCanvasElement>) => {
// Prevent click after drag
if (suppressClickRef.current) {
suppressClickRef.current = false; // reset after suppressing once
return;
}
const pointer = getCanvasPointer(event.clientX, event.clientY);
if (!pointer) {
return;
}
const { cssX, cssY } = pointer;
const clickedSpan = findSpanAtPosition(cssX, cssY);
if (!clickedSpan) {
return;
}
const searchParams = new URLSearchParams(search);
const currentSpanId = searchParams.get('spanId');
if (currentSpanId !== clickedSpan.span.spanId) {
searchParams.set('spanId', clickedSpan.span.spanId);
history.replace({ search: searchParams.toString() });
}
},
[search, history, findSpanAtPosition, getCanvasPointer],
);
const isOverFlamegraphRef = useRef(false);
useEffect(() => {
const onWheel = (e: WheelEvent) => {
// Pinch zoom on trackpads often comes as ctrl+wheel
if (isOverFlamegraphRef.current && e.ctrlKey) {
e.preventDefault(); // stops browser zoom
}
};
// capture:true ensures we intercept early
window.addEventListener('wheel', onWheel, { passive: false, capture: true });
return () => {
window.removeEventListener(
'wheel',
onWheel as any,
{ capture: true } as any,
);
};
}, []);
const wheelDeltaRef = useRef(0);
const rafRef = useRef<number | null>(null);
const lastCursorXRef = useRef(0);
const lastCssWidthRef = useRef(1);
const lastIsPinchRef = useRef(false);
const applyWheelZoom = useCallback(() => {
rafRef.current = null;
const cssWidth = lastCssWidthRef.current || 1;
const cursorX = lastCursorXRef.current;
const fullSpan = traceMetadata.endTime - traceMetadata.startTime;
const oldSpan = viewEndRef.current - viewStartRef.current;
// ✅ Different intensity for pinch vs scroll
const zoomIntensityScroll = 0.0015;
const zoomIntensityPinch = 0.01; // pinch needs stronger response
const zoomIntensity = lastIsPinchRef.current
? zoomIntensityPinch
: zoomIntensityScroll;
const deltaY = wheelDeltaRef.current;
wheelDeltaRef.current = 0;
// ✅ Smooth zoom using delta magnitude
const zoomFactor = Math.exp(deltaY * zoomIntensity);
const newSpan = oldSpan * zoomFactor;
console.log('newSpan', { cssWidth, newSpan, zoomFactor, oldSpan });
// ✅ Better minSpan clamp (absolute + pixel-based)
const absoluteMinSpan = 5; // ms
const pixelMinSpan = fullSpan / cssWidth; // ~1px of time
const minSpan = Math.max(absoluteMinSpan, pixelMinSpan);
const maxSpan = fullSpan;
const clampedSpan = Math.max(minSpan, Math.min(maxSpan, newSpan));
// ✅ Anchor preserving zoom (same as your original logic)
const cursorRatio = Math.max(0, Math.min(cursorX / cssWidth, 1));
const anchorTs = viewStartRef.current + cursorRatio * oldSpan;
const newViewStart = anchorTs - cursorRatio * clampedSpan;
const finalStart = Math.max(
traceMetadata.startTime,
Math.min(newViewStart, traceMetadata.endTime - clampedSpan),
);
const finalEnd = finalStart + clampedSpan;
console.log('finalStart', finalStart);
console.log('finalEnd', finalEnd);
setViewStartTs(finalStart);
setViewEndTs(finalEnd);
}, [traceMetadata]);
const handleWheel = useCallback(
(event: React.WheelEvent<HTMLCanvasElement>) => {
event.preventDefault();
const pointer = getCanvasPointer(event.clientX, event.clientY);
if (!pointer) {
return;
}
console.log('pointer', pointer);
const { cssX: cursorX, cssWidth } = pointer;
// ✅ Detect pinch on Chrome/Edge: ctrlKey true for trackpad pinch
lastIsPinchRef.current = event.ctrlKey;
lastCssWidthRef.current = cssWidth;
lastCursorXRef.current = cursorX;
// ✅ Accumulate deltas; apply once per frame
wheelDeltaRef.current += event.deltaY;
if (rafRef.current == null) {
rafRef.current = requestAnimationFrame(applyWheelZoom);
}
},
[applyWheelZoom, getCanvasPointer],
);
// Reset zoom and pan
const handleResetZoom = useCallback(() => {
setViewStartTs(traceMetadata.startTime);
setViewEndTs(traceMetadata.endTime);
}, [traceMetadata]);
// Handle scroll for pagination
const handleScroll = useCallback(
(event: React.UIEvent<HTMLDivElement>): void => {
const target = event.currentTarget;
setScrollTop(target.scrollTop);
// Pagination logic
if (spans.length < 50) {
return;
}
const scrollPercentage = target.scrollTop / target.scrollHeight;
const totalLevels = spans.length;
if (scrollPercentage === 0 && spans[0]?.[0]?.level !== 0) {
setFirstSpanAtFetchLevel(spans[0][0].spanId);
}
if (scrollPercentage >= 0.95 && spans[totalLevels - 1]?.[0]?.spanId) {
setFirstSpanAtFetchLevel(spans[totalLevels - 1][0].spanId);
}
},
[spans, setFirstSpanAtFetchLevel],
);
// Auto-scroll to selected span
useEffect(() => {
if (!firstSpanAtFetchLevel || !containerRef.current) {
return;
}
const levelIndex = spans.findIndex(
(level) => level[0]?.spanId === firstSpanAtFetchLevel,
);
if (levelIndex !== -1) {
const targetScroll = levelIndex * ROW_HEIGHT;
containerRef.current.scrollTop = targetScroll;
setScrollTop(targetScroll);
}
}, [firstSpanAtFetchLevel, spans]);
return (
<>
<div
ref={containerRef}
className="trace-flamegraph trace-flamegraph-canvas"
onScroll={handleScroll}
>
{(viewStartTs !== traceMetadata.startTime ||
viewEndTs !== traceMetadata.endTime) && (
<Button
className="flamegraph-reset-zoom"
size="small"
onClick={handleResetZoom}
title="Reset zoom and pan"
>
Reset View
</Button>
)}
<canvas
ref={canvasRef}
style={{
display: 'block',
width: '100%',
height: `${totalHeight}px`,
}}
onMouseMove={handleMouseMove}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseEnter={() => (isOverFlamegraphRef.current = true)}
onMouseLeave={handleMouseLeave}
onClick={handleClick}
onWheel={handleWheel}
onContextMenu={(e): void => e.preventDefault()}
/>
{tooltipContent && (
<div
style={{
position: 'fixed',
left: `${tooltipContent.x + 10}px`,
top: `${tooltipContent.y - 10}px`,
pointerEvents: 'none',
zIndex: 1000,
backgroundColor: isDarkMode ? '#1f2937' : '#ffffff',
color: isDarkMode ? '#ffffff' : '#000000',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
}}
>
{tooltipContent.content}
</div>
)}
</div>
<TimelineV2
startTimestamp={viewStartTs}
endTimestamp={viewEndTs}
offsetTimestamp={viewStartTs - traceMetadata.startTime}
timelineHeight={22}
/>
</>
);
}
export default Success;
// on drag on click is getting registered as a click
// zoom and scale not matching
// check minimap logic
// use
// const scrollTopRef = useRef(scrollTop);
// useEffect(() => {
// scrollTopRef.current = scrollTop;
// }, [scrollTop]);
// fix clicks in interaction canvas
// Auto-scroll to selected span else on top(based on default span)
// time bar line vertical
// zoom handle vertical and horizontal scroll with proper defined thresholds
// timeline should be in sync with the flamegraph. test with vertical line of time on event etc.
// proper working interaction layer for clicks and hovers
// hit testing should be efficient and accurate without flat spanRect
// Final Priority Order (Clean Summary)
// ✅ Zoom (Horizontal + Vertical thresholds)
// ✅ Timeline sync + vertical time dashed line
// ✅ Minimap brush correctness
// ✅ Auto-scroll behavior
// ✅ Interaction layer separation
// ✅ Efficient hit testing

View File

@@ -17,9 +17,6 @@ const useGetTraceFlamegraph = (
REACT_QUERY_KEY.GET_TRACE_V2_FLAMEGRAPH,
props.traceId,
props.selectedSpanId,
props.limit,
props.boundaryStartTsMilli,
props.boundarEndTsMilli,
],
enabled: !!props.traceId,
keepPreviousData: true,

View File

@@ -5,9 +5,6 @@ export interface TraceDetailFlamegraphURLProps {
export interface GetTraceFlamegraphPayloadProps {
traceId: string;
selectedSpanId: string;
limit: number;
boundaryStartTsMilli: number;
boundarEndTsMilli: number;
}
export interface Event {
@@ -34,6 +31,4 @@ export interface GetTraceFlamegraphSuccessResponse {
spans: FlamegraphSpan[][];
startTimestampMillis: number;
endTimestampMillis: number;
durationNano: number;
hasMore: boolean;
}

View File

@@ -1046,19 +1046,7 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con
}
processingPostCache := time.Now()
limit := min(req.Limit, tracedetail.MaxLimitToSelectAllSpans)
selectAllSpans := totalSpans <= uint64(limit)
var (
selectedSpans []*model.Span
uncollapsedSpans []string
rootServiceName, rootServiceEntryPoint string
)
if selectAllSpans {
selectedSpans, rootServiceName, rootServiceEntryPoint = tracedetail.GetAllSpans(traceRoots)
} else {
selectedSpans, uncollapsedSpans, rootServiceName, rootServiceEntryPoint = tracedetail.GetSelectedSpans(req.UncollapsedSpans, req.SelectedSpanID, traceRoots, spanIdToSpanNodeMap, req.IsSelectedSpanIDUnCollapsed)
}
selectedSpans, uncollapsedSpans, rootServiceName, rootServiceEntryPoint := tracedetail.GetSelectedSpans(req.UncollapsedSpans, req.SelectedSpanID, traceRoots, spanIdToSpanNodeMap, req.IsSelectedSpanIDUnCollapsed)
zap.L().Info("getWaterfallSpansForTraceWithMetadata: processing post cache", zap.Duration("duration", time.Since(processingPostCache)), zap.String("traceID", traceID))
// convert start timestamp to millis because right now frontend is expecting it in millis
@@ -1071,7 +1059,7 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con
}
response.Spans = selectedSpans
response.UncollapsedSpans = uncollapsedSpans // ignoring if all spans are returning
response.UncollapsedSpans = uncollapsedSpans
response.StartTimestampMillis = startTime / 1000000
response.EndTimestampMillis = endTime / 1000000
response.TotalSpansCount = totalSpans
@@ -1080,7 +1068,6 @@ func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Con
response.RootServiceEntryPoint = rootServiceEntryPoint
response.ServiceNameToTotalDurationMap = serviceNameToTotalDurationMap
response.HasMissingSpans = hasMissingSpans
response.HasMore = !selectAllSpans
return response, nil
}
@@ -1212,7 +1199,7 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, orgID
}
}
selectedSpans = tracedetail.GetAllSpansForFlamegraph(traceRoots, spanIdToSpanNodeMap)
selectedSpans = tracedetail.GetSelectedSpansForFlamegraph(traceRoots, spanIdToSpanNodeMap)
traceCache := model.GetFlamegraphSpansForTraceCache{
StartTime: startTime,
EndTime: endTime,
@@ -1229,20 +1216,12 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, orgID
}
processingPostCache := time.Now()
selectedSpansForRequest := selectedSpans
limit := min(req.Limit, tracedetail.MaxLimitWithoutSampling)
totalSpanCount := tracedetail.GetTotalSpanCount(selectedSpans)
if totalSpanCount > uint64(limit) {
boundaryStart, boundaryEnd := utils.MilliToNano(req.BoundaryStartTS), utils.MilliToNano(req.BoundaryEndTS)
selectedSpansForRequest = tracedetail.GetSelectedSpansForFlamegraphForRequest(req.SelectedSpanID, selectedSpans, boundaryStart, boundaryEnd)
}
zap.L().Info("getFlamegraphSpansForTrace: processing post cache", zap.Duration("duration", time.Since(processingPostCache)), zap.String("traceID", traceID),
zap.Uint64("totalSpanCount", totalSpanCount))
selectedSpansForRequest := tracedetail.GetSelectedSpansForFlamegraphForRequest(req.SelectedSpanID, selectedSpans, startTime, endTime)
zap.L().Info("getFlamegraphSpansForTrace: processing post cache", zap.Duration("duration", time.Since(processingPostCache)), zap.String("traceID", traceID))
trace.Spans = selectedSpansForRequest
trace.StartTimestampMillis = startTime / 1000000
trace.EndTimestampMillis = endTime / 1000000
trace.HasMore = totalSpanCount > uint64(limit)
return trace, nil
}

View File

@@ -7,11 +7,9 @@ import (
)
var (
flamegraphSpanLevelLimit float64 = 50
flamegraphSpanLimitPerLevel int = 1000
flamegraphSamplingBucketCount int = 500
MaxLimitWithoutSampling uint = 120_000
SPAN_LIMIT_PER_REQUEST_FOR_FLAMEGRAPH float64 = 50
SPAN_LIMIT_PER_LEVEL int = 100
TIMESTAMP_SAMPLING_BUCKET_COUNT int = 50
)
func ContainsFlamegraphSpan(slice []*model.FlamegraphSpan, item *model.FlamegraphSpan) bool {
@@ -54,8 +52,7 @@ func FindIndexForSelectedSpan(spans [][]*model.FlamegraphSpan, selectedSpanId st
return selectedSpanLevel
}
// GetAllSpansForFlamegraph groups all spans as per their level
func GetAllSpansForFlamegraph(traceRoots []*model.FlamegraphSpan, spanIdToSpanNodeMap map[string]*model.FlamegraphSpan) [][]*model.FlamegraphSpan {
func GetSelectedSpansForFlamegraph(traceRoots []*model.FlamegraphSpan, spanIdToSpanNodeMap map[string]*model.FlamegraphSpan) [][]*model.FlamegraphSpan {
var traceIdLevelledFlamegraph = map[string]map[int64][]*model.FlamegraphSpan{}
selectedSpans := [][]*model.FlamegraphSpan{}
@@ -103,7 +100,7 @@ func getLatencyAndTimestampBucketedSpans(spans []*model.FlamegraphSpan, selected
})
// pick the top 5 latency spans
for idx := range 100 {
for idx := range 5 {
sampledSpans = append(sampledSpans, spans[idx])
}
@@ -113,7 +110,6 @@ func getLatencyAndTimestampBucketedSpans(spans []*model.FlamegraphSpan, selected
for _idx, span := range spans {
if span.SpanID == selectedSpanID {
idx = _idx
break
}
}
if idx != -1 {
@@ -121,17 +117,17 @@ func getLatencyAndTimestampBucketedSpans(spans []*model.FlamegraphSpan, selected
}
}
bucketSize := (endTime - startTime) / uint64(flamegraphSamplingBucketCount)
bucketSize := (endTime - startTime) / uint64(TIMESTAMP_SAMPLING_BUCKET_COUNT)
if bucketSize == 0 {
bucketSize = 1
}
bucketedSpans := make([][]*model.FlamegraphSpan, flamegraphSamplingBucketCount)
bucketedSpans := make([][]*model.FlamegraphSpan, 50)
for _, span := range spans {
if span.TimeUnixNano >= startTime && span.TimeUnixNano <= endTime {
bucketIndex := int((span.TimeUnixNano - startTime) / bucketSize)
if bucketIndex >= 0 && bucketIndex < flamegraphSamplingBucketCount {
if bucketIndex >= 0 && bucketIndex < 50 {
bucketedSpans[bucketIndex] = append(bucketedSpans[bucketIndex], span)
}
}
@@ -160,8 +156,8 @@ func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpan
selectedIndex = FindIndexForSelectedSpan(selectedSpans, selectedSpanID)
}
lowerLimit := selectedIndex - int(flamegraphSpanLevelLimit*0.4)
upperLimit := selectedIndex + int(flamegraphSpanLevelLimit*0.6)
lowerLimit := selectedIndex - int(SPAN_LIMIT_PER_REQUEST_FOR_FLAMEGRAPH*0.4)
upperLimit := selectedIndex + int(SPAN_LIMIT_PER_REQUEST_FOR_FLAMEGRAPH*0.6)
if lowerLimit < 0 {
upperLimit = upperLimit - lowerLimit
@@ -178,7 +174,7 @@ func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpan
}
for i := lowerLimit; i < upperLimit; i++ {
if len(selectedSpans[i]) > flamegraphSpanLimitPerLevel {
if len(selectedSpans[i]) > SPAN_LIMIT_PER_LEVEL {
_spans := getLatencyAndTimestampBucketedSpans(selectedSpans[i], selectedSpanID, i == selectedIndex, startTime, endTime)
selectedSpansForRequest = append(selectedSpansForRequest, _spans)
} else {
@@ -188,12 +184,3 @@ func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpan
return selectedSpansForRequest
}
func GetTotalSpanCount(spans [][]*model.FlamegraphSpan) uint64 {
levelCount := len(spans)
spanCount := uint64(0)
for i := range levelCount {
spanCount += uint64(len(spans[i]))
}
return spanCount
}

View File

@@ -9,9 +9,6 @@ import (
var (
SPAN_LIMIT_PER_REQUEST_FOR_WATERFALL float64 = 500
maxDepthForSelectedSpanChildren int = 5
MaxLimitToSelectAllSpans uint = 10_000
)
type Interval struct {
@@ -91,11 +88,8 @@ func getPathFromRootToSelectedSpanId(node *model.Span, selectedSpanId string, un
return isPresentInSubtreeForTheNode, spansFromRootToNode
}
func traverseTrace(span *model.Span, uncollapsedSpans []string, level uint64, isPartOfPreOrder bool, hasSibling bool, selectedSpanId string,
depthFromSelectedSpan int, isSelectedSpanIDUnCollapsed bool, selectAllSpan bool) ([]*model.Span, []string) {
func traverseTrace(span *model.Span, uncollapsedSpans []string, level uint64, isPartOfPreOrder bool, hasSibling bool, selectedSpanId string) []*model.Span {
preOrderTraversal := []*model.Span{}
autoExpandedSpans := []string{}
// sort the children to maintain the order across requests
sort.Slice(span.Children, func(i, j int) bool {
@@ -132,40 +126,15 @@ func traverseTrace(span *model.Span, uncollapsedSpans []string, level uint64, is
preOrderTraversal = append(preOrderTraversal, &nodeWithoutChildren)
}
nextDepthFromSelectedSpan := -1
if span.SpanID == selectedSpanId && isSelectedSpanIDUnCollapsed {
nextDepthFromSelectedSpan = 1
} else if depthFromSelectedSpan >= 1 && depthFromSelectedSpan < maxDepthForSelectedSpanChildren {
nextDepthFromSelectedSpan = depthFromSelectedSpan + 1
}
for index, child := range span.Children {
// A child is included in the pre-order output if its parent is uncollapsed
// OR if the child falls within MAX_DEPTH_FOR_SELECTED_SPAN_CHILDREN levels
// below the selected span.
isChildWithinMaxDepth := nextDepthFromSelectedSpan >= 1
isAlreadyUncollapsed := slices.Contains(uncollapsedSpans, span.SpanID)
childIsPartOfPreOrder := isPartOfPreOrder && (isAlreadyUncollapsed || isChildWithinMaxDepth)
if selectAllSpan {
childIsPartOfPreOrder = true
}
if isPartOfPreOrder && isChildWithinMaxDepth && !isAlreadyUncollapsed {
if !slices.Contains(autoExpandedSpans, span.SpanID) {
autoExpandedSpans = append(autoExpandedSpans, span.SpanID)
}
}
_childTraversal, _autoExpanded := traverseTrace(child, uncollapsedSpans, level+1, childIsPartOfPreOrder, index != (len(span.Children)-1), selectedSpanId,
nextDepthFromSelectedSpan, isSelectedSpanIDUnCollapsed, selectAllSpan)
_childTraversal := traverseTrace(child, uncollapsedSpans, level+1, isPartOfPreOrder && slices.Contains(uncollapsedSpans, span.SpanID), index != (len(span.Children)-1), selectedSpanId)
preOrderTraversal = append(preOrderTraversal, _childTraversal...)
autoExpandedSpans = append(autoExpandedSpans, _autoExpanded...)
nodeWithoutChildren.SubTreeNodeCount += child.SubTreeNodeCount + 1
span.SubTreeNodeCount += child.SubTreeNodeCount + 1
}
nodeWithoutChildren.SubTreeNodeCount += 1
return preOrderTraversal, autoExpandedSpans
return preOrderTraversal
}
@@ -199,13 +168,7 @@ func GetSelectedSpans(uncollapsedSpans []string, selectedSpanID string, traceRoo
_, spansFromRootToNode := getPathFromRootToSelectedSpanId(rootNode, selectedSpanID, updatedUncollapsedSpans, isSelectedSpanIDUnCollapsed)
updatedUncollapsedSpans = append(updatedUncollapsedSpans, spansFromRootToNode...)
_preOrderTraversal, _autoExpanded := traverseTrace(rootNode, updatedUncollapsedSpans, 0, true, false, selectedSpanID, -1, isSelectedSpanIDUnCollapsed, false)
// Merge auto-expanded spans into updatedUncollapsedSpans for returning in response
for _, spanID := range _autoExpanded {
if !slices.Contains(updatedUncollapsedSpans, spanID) {
updatedUncollapsedSpans = append(updatedUncollapsedSpans, spanID)
}
}
_preOrderTraversal := traverseTrace(rootNode, updatedUncollapsedSpans, 0, true, false, selectedSpanID)
_selectedSpanIndex := findIndexForSelectedSpanFromPreOrder(_preOrderTraversal, selectedSpanID)
if _selectedSpanIndex != -1 {
@@ -249,17 +212,3 @@ func GetSelectedSpans(uncollapsedSpans []string, selectedSpanID string, traceRoo
return preOrderTraversal[startIndex:endIndex], updatedUncollapsedSpans, rootServiceName, rootServiceEntryPoint
}
func GetAllSpans(traceRoots []*model.Span) (spans []*model.Span, rootServiceName, rootEntryPoint string) {
for _, root := range traceRoots {
childSpans, _ := traverseTrace(root, nil, 0, true, false, "", -1, false, true)
spans = append(spans, childSpans...)
if rootServiceName == "" {
rootServiceName = root.ServiceName
}
if rootEntryPoint == "" {
rootEntryPoint = root.Name
}
}
return
}

View File

@@ -333,14 +333,10 @@ type GetWaterfallSpansForTraceWithMetadataParams struct {
SelectedSpanID string `json:"selectedSpanId"`
IsSelectedSpanIDUnCollapsed bool `json:"isSelectedSpanIDUnCollapsed"`
UncollapsedSpans []string `json:"uncollapsedSpans"`
Limit uint `json:"limit"`
}
type GetFlamegraphSpansForTraceParams struct {
SelectedSpanID string `json:"selectedSpanId"`
Limit uint `json:"limit"`
BoundaryStartTS uint64 `json:"boundaryStartTsMilli"`
BoundaryEndTS uint64 `json:"boundarEndTsMilli"`
SelectedSpanID string `json:"selectedSpanId"`
}
type SpanFilterParams struct {

View File

@@ -329,7 +329,6 @@ type GetWaterfallSpansForTraceWithMetadataResponse struct {
HasMissingSpans bool `json:"hasMissingSpans"`
// this is needed for frontend and query service sync
UncollapsedSpans []string `json:"uncollapsedSpans"`
HasMore bool `json:"hasMore"`
}
type GetFlamegraphSpansForTraceResponse struct {
@@ -337,7 +336,6 @@ type GetFlamegraphSpansForTraceResponse struct {
EndTimestampMillis uint64 `json:"endTimestampMillis"`
DurationNano uint64 `json:"durationNano"`
Spans [][]*FlamegraphSpan `json:"spans"`
HasMore bool `json:"hasMore"`
}
type OtelSpanRef struct {

View File

@@ -17,7 +17,3 @@ func Elapsed(funcName string, args map[string]interface{}) func() {
zap.L().Info("Elapsed time", zapFields...)
}
}
func MilliToNano(milliTS uint64) uint64 {
return milliTS * 1000_000
}