signoz/frontend/src/hooks/TracesFunnels/useFunnelGraph.tsx
Shaheer Kochai 6334e09a60
feat: Funnel Details Page Base Structure (#7364)
* feat: funnels list page basic UI

* feat: get funnels list data from mock API, and handle data, loading and empty states

* feat: implement funnel rename

* chore: move useFunnels to hooks/TracesFunnels

* feat: create traces funnels details basic page + funnel -> details redirection

* fix: properly display created at in funnels list item + preventDefault

* chore: add tab bar to trace funnel details page

* chore: traces funnel details page overall skeleton

* chore: traces funnel details results skeleton

* fix: hide step count for add button only

* feat: funnel details page steps and configuration (#7424)

* chore: add a new tab for traces funnels

* feat: funnels list page basic UI

* feat: get funnels list data from mock API, and handle data, loading and empty states

* feat: implement funnel rename

* refactor: overall improvements

* feat: implement sorting in traces funnels list page

* feat: add sort column key and order to url params

* chore: move useFunnels to hooks/TracesFunnels

* feat: implement traces funnels search and refactor search and sort by extracting to custom hooks

* chore: overall improvements to rename trace funnel modal

* chore: make the rename input auto-focusable

* feat: handle create funnel modal

* feat: delete funnel modal and functionality

* fix: fix the layout shift in funnel item caused by getContainer={false}

* chore: overall improvements and use live api in traces funnels

* feat: create traces funnels details basic page + funnel -> details redirection

* fix: funnels traces light mode UI

* fix: properly display created at in funnels list item + preventDefault

* refactor: extract FunnelItemPopover into a separate component

* chore: hide funnel tab from traces explorer

* chore: add check to display trace funnels tab only in dev environment

* chore: improve funnels modals light mode

* chore: overall improvements

* fix: properly pass funnel details link

* chore: address PR review changes

* chore: add tab bar to trace funnel details page

* feat: funnel step UI with service, span, and where filters

* feat: build radio button component

* refactor: use the SignozRadioButton in funnel results -> step transitions radio buttons

* feat: inter step config (i.e. latency type) UI

* chore: improve steps header styles by removing divider width

* feat: funnel steps title, description, popover UI + pass data from API

* chore: update FilterSelect component to conditionally add url params and accept on change

* fix: fix funnel step where clause and update the state variables for filters

* chore: add support for isMultiple and fix the type in FilterSelect

* feat: centralize the steps state management in StepsContent

* fix: move steps state up + pass steps count from state

* feat: implement auto save for updating the steps whenever any step changes

* feat: implement auto save for validating steps if service name or span names change

* feat: impelement funnel step removal

* feat: implement add details modal for funnel steps

* fix: fix the overflowing time range picker

* feat: funnel details empty state

* feat: add support for saving funnel description

* chore: overall improvements

* fix: fix the light mode styles

* fix: fix the failing build + broken search UI

* refactor: remove the reference of useLocation from traceFunnel item in TraceModulePage constant

* fix: fix the issue of update steps getting triggered on initial render if we have filters

* fix: fix the edge case of stale state causing filters to be re-added after removing

* feat: funnel details page results (#7451)

* feat: funnel metrics table component

* feat: funnel metrics and steps transition metrics components UI

* feat: funnel table component

* feat: slowest traces and traces with error components

* fix: overall light theme fixes

* fix: fix the warning

* chore: add empty and loading states to FunnelMetricsTable

* feat: get overall funnel metrics from the API

* fix: fix the empty state of funnel metrics table

* feat: get data for slowest traces and traces with errors

* fix: link trace id to trace details page

* fix: get data for funnel step transition metrics and refactor the existing data fetching logic

* refactor: add funnel context + overall refactoring and optimizations

* refactor: move steps states to funnel context + handle empty and run funnel disabled states

* feat: handle run funnel

* fix: improve empty state

* chore: rename isValidateStepsMutationLoading -> isValidateStepsLoading

* chore: improve query key

* fix: display loading state if funnel results are fetching

* refactor: move steps validation fetching and states to the context API

* fix: display loading state in funnel results while steps validation is fetching

* fix: call validate steps API only on changing the service name or span name of any step

* refactor: move validateStepsQuery key out of useEffect and update the dependencies

* chore: centralize hasIncompleteSteps and run validate only if steps have service and spans

* fix: handle all empty fields state + overall improvements

* fix: handle long where query tags

* feat: build the funnel result graph component

* feat: build the funnel result graph component

* feat: handle loading, error, empty states in funnel graph

* fix: don't display change percentage if % is 0

* refactor: overall improvements

* feat: get funnel steps graph data from API + move logic to custom hook

* fix: improve empty and error states

* fix: handle funnel graph legends width using css

* fix: redirect to trace funnels list page on clicking delete from funnel details

* fix: update the query cache while updating steps

* fix: implement debounced search for funnel list search

* fix: refetch steps graph data query on clicking run funnel / sync button

* fix: improve the step footer spacing

* chore: add gap between divider to inter-step-config

* fix: handle loading state while fetching

* feat: add span to funnel flow (from trace details page) (#7477)

* chore: display add to funnel icon on hovering any span in trace details page

* chore: add className to funnel item actions popover

* feat: add funnels tab to trace details v2 tab bar

* feat: add span to funnel flow

* chore: hide actions popover button from funnel item in span -> funnel flows

* chore: improve the funnel details UI in add span to funnel modal

* fix: display empty state + don't redirect to funnels list on delete success + overall improvements

* chore: add null check

* fix: display add to funnel button based on feature flag

* fix: display funnels tab in trace details based on feature flag

* fix: remove maxTagCount

* feat: change ms to ns

* chore: address review comments

* chore: remove feature flag and display trace funnels only in dev envirnoment

* fix: handle restoring steps if updating funnel steps fail

* refactor: update the get and delete funnel endpoints to adjust to the BE changes (#7697)

* refactor: address review comments

* fix: handle nested funnel response structure to fix missing funnel_id… (#7740)

* fix: handle nested funnel response structure to fix missing funnel_id in updates

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: remove console.og

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* chore: revert explicitly passing funnelId to updateFunnelSteps

---------

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
Co-authored-by: ahmadshaheer <ashaheerki@gmail.com>

* chore: fix api endpoint

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>

* refactor: incorporate the recent funnel details API changes (#7760)

* chore: trace funnels feedback changes (#7772)

* chore: change the copy from x traces to valid traces found / not found

* chore: add open funnel button in add span to funnel modal

* feat: display buttons for adding step details and funnel description + copy to clipboard

* feat: highlight funnel graph column based on selected (total / error span) from the legend items

* chore: trace funnel changes (#7780)

* refactor: handle funnels list search on frontend

* refactor: use funnel steps update API for adding / updating step title and description

* feat: allow selecting user's typed option in trace funnel service and span name dropdowns

* chore: properly render the -> between steps in funnel results

* fix: sync funnel step name with add details modal text fields

---------

Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
Co-authored-by: Yunus M <myounis.ar@live.com>
Co-authored-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-05-12 10:16:26 +05:30

264 lines
6.3 KiB
TypeScript

import { Color } from '@signozhq/design-tokens';
import { FunnelStepGraphMetrics } from 'api/traceFunnels';
import { Chart, ChartConfiguration } from 'chart.js';
import ChangePercentagePill from 'components/ChangePercentagePill/ChangePercentagePill';
import { useCallback, useEffect, useRef, useState } from 'react';
const CHART_CONFIG: Partial<ChartConfiguration> = {
type: 'bar',
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true,
grid: {
display: false,
},
ticks: {
font: {
family: "'Geist Mono', monospace",
},
},
},
y: {
stacked: true,
beginAtZero: true,
grid: {
color: 'rgba(192, 193, 195, 0.04)',
},
ticks: {
font: {
family: "'Geist Mono', monospace",
},
},
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
},
},
},
};
interface UseFunnelGraphProps {
data: FunnelStepGraphMetrics | undefined;
hoveredBar?: { index: number; type: 'total' | 'error' } | null;
}
interface UseFunnelGraph {
successSteps: number[];
errorSteps: number[];
totalSteps: number;
canvasRef: React.RefObject<HTMLCanvasElement>;
renderLegendItem: (
step: number,
successSpans: number,
errorSpans: number,
prevTotalSpans: number,
legendHoverHandlers?: {
onTotalHover: () => void;
onErrorHover: () => void;
onLegendLeave: () => void;
},
) => JSX.Element;
}
function useFunnelGraph({
data,
hoveredBar,
}: UseFunnelGraphProps): UseFunnelGraph {
const canvasRef = useRef<HTMLCanvasElement>(null);
const chartRef = useRef<Chart | null>(null);
const [localHoveredBar, setLocalHoveredBar] = useState<{
index: number;
type: 'total' | 'error';
} | null>(null);
const getPercentageChange = useCallback(
(current: number, previous: number): number => {
if (previous === 0) return 0;
return Math.abs(Math.round(((current - previous) / previous) * 100));
},
[],
);
interface StepGraphData {
successSteps: number[];
errorSteps: number[];
totalSteps: number;
}
const getStepGraphData = useCallback((): StepGraphData => {
const successSteps: number[] = [];
const errorSteps: number[] = [];
let stepCount = 1;
if (!data) return { successSteps, errorSteps, totalSteps: 0 };
while (
data[`total_s${stepCount}_spans`] !== undefined &&
data[`total_s${stepCount}_errored_spans`] !== undefined
) {
const totalSpans = data[`total_s${stepCount}_spans`];
const erroredSpans = data[`total_s${stepCount}_errored_spans`];
const successSpans = totalSpans - erroredSpans;
successSteps.push(successSpans);
errorSteps.push(erroredSpans);
stepCount += 1;
}
return {
successSteps,
errorSteps,
totalSteps: stepCount - 1,
};
}, [data]);
useEffect(() => {
if (!canvasRef.current) return;
if (chartRef.current) {
chartRef.current.destroy();
}
const ctx = canvasRef.current.getContext('2d');
if (!ctx) return;
const { successSteps, errorSteps, totalSteps } = getStepGraphData();
chartRef.current = new Chart(ctx, {
...CHART_CONFIG,
data: {
labels: Array.from({ length: totalSteps }, (_, i) => String(i + 1)),
datasets: [
{
label: 'Success spans',
data: successSteps,
backgroundColor: successSteps.map(() => Color.BG_ROBIN_500),
stack: 'Stack 0',
borderRadius: 2,
borderSkipped: false,
},
{
label: 'Error spans',
data: errorSteps,
backgroundColor: errorSteps.map(() => Color.BG_CHERRY_500),
stack: 'Stack 0',
borderRadius: 2,
borderSkipped: false,
borderWidth: {
top: 2,
bottom: 2,
},
borderColor: 'rgba(0, 0, 0, 0)',
},
],
},
options: CHART_CONFIG.options,
} as ChartConfiguration);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
useEffect(() => {
const chart = chartRef.current;
if (!chart) return;
const { successSteps, errorSteps } = getStepGraphData();
if (chart.data.datasets && chart.data.datasets.length >= 2) {
chart.data.datasets[0].backgroundColor = successSteps.map((_, i) =>
localHoveredBar &&
localHoveredBar.index === i &&
localHoveredBar.type === 'total'
? '#2655ff'
: Color.BG_ROBIN_500,
);
chart.data.datasets[1].backgroundColor = errorSteps.map((_, i) =>
localHoveredBar &&
localHoveredBar.index === i &&
localHoveredBar.type === 'error'
? '#ff1018'
: Color.BG_CHERRY_500,
);
chart.update();
}
}, [localHoveredBar, getStepGraphData]);
useEffect(() => {
setLocalHoveredBar(hoveredBar ?? null);
}, [hoveredBar]);
const renderLegendItem = useCallback(
(
step: number,
successSpans: number,
errorSpans: number,
prevTotalSpans: number,
legendHoverHandlers?: {
onTotalHover: () => void;
onErrorHover: () => void;
onLegendLeave: () => void;
},
): JSX.Element => {
const totalSpans = successSpans + errorSpans;
return (
<div key={step} className="funnel-graph__legend-column">
<div
className="legend-item"
onMouseEnter={legendHoverHandlers?.onTotalHover}
onMouseLeave={legendHoverHandlers?.onLegendLeave}
>
<div className="legend-item__left">
<span className="legend-item__dot legend-item--total" />
<span className="legend-item__label">Total spans</span>
</div>
<div className="legend-item__right">
<span className="legend-item__value">{totalSpans}</span>
{step > 1 && (
<ChangePercentagePill
direction={totalSpans < prevTotalSpans ? -1 : 1}
percentage={getPercentageChange(totalSpans, prevTotalSpans)}
/>
)}
</div>
</div>
<div
className="legend-item"
onMouseEnter={legendHoverHandlers?.onErrorHover}
onMouseLeave={legendHoverHandlers?.onLegendLeave}
>
<div className="legend-item__left">
<span className="legend-item__dot legend-item--error" />
<span className="legend-item__label">Error spans</span>
</div>
<div className="legend-item__right">
<span className="legend-item__value">{errorSpans}</span>
</div>
</div>
</div>
);
},
[getPercentageChange],
);
const { successSteps, errorSteps, totalSteps } = getStepGraphData();
return {
successSteps,
errorSteps,
totalSteps,
canvasRef,
renderLegendItem,
};
}
export default useFunnelGraph;