mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 03:25:57 +08:00
fix: trace funnel bugfixes and improvements (#7922)
* fix: display the inter-step latency type in step metrics table * chore: send latency type with n+1th step + make latency type optional * fix: fetch and format get funnel steps overview metrics * chore: remove dev env check * fix: overall fixes * fix: don't cache validate query + trigger validate on changing error and where clause as well * fix: display the latency type in step overview metrics table + p99_latency to latency * chore: revert dev env check removal (remove after BE changes are merged) * fix: adjust create API response * chore: useLocalStorage custom hook * feat: improve the run funnel flow - for the initial fetch of funnel results, require the user to run the funnel - subsequently change the run funnel button to a refresh button - display loading state while any of the funnel results APIs are being fetched * fix: fix the issue of add step details breaking * fix: refetch funnel details on rename success * fix: redirect 'learn more' to trace funnels docs * fix: handle potential undefined step in latency type calculation * fix: properly handle incomplete steps state * fix: fix the edge case of stale validation state on transitioning from invalid steps to valid steps * fix: remove the side effect from render and move to useEffect
This commit is contained in:
parent
354e4b4b8f
commit
57f96574ff
@ -22,7 +22,7 @@ export const createFunnel = async (
|
|||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
error: null,
|
error: null,
|
||||||
message: 'Funnel created successfully',
|
message: 'Funnel created successfully',
|
||||||
payload: response.data,
|
payload: response.data.data,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -196,7 +196,9 @@ export interface FunnelOverviewResponse {
|
|||||||
avg_rate: number;
|
avg_rate: number;
|
||||||
conversion_rate: number | null;
|
conversion_rate: number | null;
|
||||||
errors: number;
|
errors: number;
|
||||||
|
// TODO(shaheer): remove p99_latency once we have support for latency
|
||||||
p99_latency: number;
|
p99_latency: number;
|
||||||
|
latency: number;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
@ -222,13 +224,6 @@ export const getFunnelOverview = async (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface SlowTracesPayload {
|
|
||||||
start_time: number;
|
|
||||||
end_time: number;
|
|
||||||
step_a_order: number;
|
|
||||||
step_b_order: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SlowTraceData {
|
export interface SlowTraceData {
|
||||||
status: string;
|
status: string;
|
||||||
data: Array<{
|
data: Array<{
|
||||||
@ -243,7 +238,7 @@ export interface SlowTraceData {
|
|||||||
|
|
||||||
export const getFunnelSlowTraces = async (
|
export const getFunnelSlowTraces = async (
|
||||||
funnelId: string,
|
funnelId: string,
|
||||||
payload: SlowTracesPayload,
|
payload: FunnelOverviewPayload,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<SuccessResponse<SlowTraceData> | ErrorResponse> => {
|
): Promise<SuccessResponse<SlowTraceData> | ErrorResponse> => {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
@ -261,12 +256,6 @@ export const getFunnelSlowTraces = async (
|
|||||||
payload: response.data,
|
payload: response.data,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export interface ErrorTracesPayload {
|
|
||||||
start_time: number;
|
|
||||||
end_time: number;
|
|
||||||
step_a_order: number;
|
|
||||||
step_b_order: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ErrorTraceData {
|
export interface ErrorTraceData {
|
||||||
status: string;
|
status: string;
|
||||||
@ -282,7 +271,7 @@ export interface ErrorTraceData {
|
|||||||
|
|
||||||
export const getFunnelErrorTraces = async (
|
export const getFunnelErrorTraces = async (
|
||||||
funnelId: string,
|
funnelId: string,
|
||||||
payload: ErrorTracesPayload,
|
payload: FunnelOverviewPayload,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<SuccessResponse<ErrorTraceData> | ErrorResponse> => {
|
): Promise<SuccessResponse<ErrorTraceData> | ErrorResponse> => {
|
||||||
const response: AxiosResponse = await axios.post(
|
const response: AxiosResponse = await axios.post(
|
||||||
@ -337,3 +326,37 @@ export const getFunnelSteps = async (
|
|||||||
payload: response.data,
|
payload: response.data,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface FunnelStepsOverviewPayload {
|
||||||
|
start_time: number;
|
||||||
|
end_time: number;
|
||||||
|
step_start?: number;
|
||||||
|
step_end?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FunnelStepsOverviewResponse {
|
||||||
|
status: string;
|
||||||
|
data: Array<{
|
||||||
|
timestamp: string;
|
||||||
|
data: Record<string, number>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getFunnelStepsOverview = async (
|
||||||
|
funnelId: string,
|
||||||
|
payload: FunnelStepsOverviewPayload,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<SuccessResponse<FunnelStepsOverviewResponse> | ErrorResponse> => {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/steps/overview`,
|
||||||
|
payload,
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: '',
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -30,4 +30,5 @@ export enum LOCALSTORAGE {
|
|||||||
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
|
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
|
||||||
BANNER_DISMISSED = 'BANNER_DISMISSED',
|
BANNER_DISMISSED = 'BANNER_DISMISSED',
|
||||||
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
|
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
|
||||||
|
UNEXECUTED_FUNNELS = 'UNEXECUTED_FUNNELS',
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,7 @@ export const REACT_QUERY_KEY = {
|
|||||||
VALIDATE_FUNNEL_STEPS: 'VALIDATE_FUNNEL_STEPS',
|
VALIDATE_FUNNEL_STEPS: 'VALIDATE_FUNNEL_STEPS',
|
||||||
UPDATE_FUNNEL_STEP_DETAILS: 'UPDATE_FUNNEL_STEP_DETAILS',
|
UPDATE_FUNNEL_STEP_DETAILS: 'UPDATE_FUNNEL_STEP_DETAILS',
|
||||||
GET_FUNNEL_OVERVIEW: 'GET_FUNNEL_OVERVIEW',
|
GET_FUNNEL_OVERVIEW: 'GET_FUNNEL_OVERVIEW',
|
||||||
|
GET_FUNNEL_STEPS_OVERVIEW: 'GET_FUNNEL_STEPS_OVERVIEW',
|
||||||
GET_FUNNEL_SLOW_TRACES: 'GET_FUNNEL_SLOW_TRACES',
|
GET_FUNNEL_SLOW_TRACES: 'GET_FUNNEL_SLOW_TRACES',
|
||||||
GET_FUNNEL_ERROR_TRACES: 'GET_FUNNEL_ERROR_TRACES',
|
GET_FUNNEL_ERROR_TRACES: 'GET_FUNNEL_ERROR_TRACES',
|
||||||
GET_FUNNEL_STEPS_GRAPH_DATA: 'GET_FUNNEL_STEPS_GRAPH_DATA',
|
GET_FUNNEL_STEPS_GRAPH_DATA: 'GET_FUNNEL_STEPS_GRAPH_DATA',
|
||||||
|
@ -43,8 +43,7 @@ export default function useFunnelConfiguration({
|
|||||||
const {
|
const {
|
||||||
steps,
|
steps,
|
||||||
initialSteps,
|
initialSteps,
|
||||||
setHasIncompleteStepFields,
|
hasIncompleteStepFields,
|
||||||
setHasAllEmptyStepFields,
|
|
||||||
handleRestoreSteps,
|
handleRestoreSteps,
|
||||||
} = useFunnelContext();
|
} = useFunnelContext();
|
||||||
|
|
||||||
@ -74,14 +73,16 @@ export default function useFunnelConfiguration({
|
|||||||
return !isEqual(normalizedDebouncedSteps, normalizedLastSavedSteps);
|
return !isEqual(normalizedDebouncedSteps, normalizedLastSavedSteps);
|
||||||
}, [debouncedSteps]);
|
}, [debouncedSteps]);
|
||||||
|
|
||||||
const hasStepServiceOrSpanNameChanged = useCallback(
|
const hasFunnelStepDefinitionsChanged = useCallback(
|
||||||
(prevSteps: FunnelStepData[], nextSteps: FunnelStepData[]): boolean => {
|
(prevSteps: FunnelStepData[], nextSteps: FunnelStepData[]): boolean => {
|
||||||
if (prevSteps.length !== nextSteps.length) return true;
|
if (prevSteps.length !== nextSteps.length) return true;
|
||||||
return prevSteps.some((step, index) => {
|
return prevSteps.some((step, index) => {
|
||||||
const nextStep = nextSteps[index];
|
const nextStep = nextSteps[index];
|
||||||
return (
|
return (
|
||||||
step.service_name !== nextStep.service_name ||
|
step.service_name !== nextStep.service_name ||
|
||||||
step.span_name !== nextStep.span_name
|
step.span_name !== nextStep.span_name ||
|
||||||
|
!isEqual(step.filters, nextStep.filters) ||
|
||||||
|
step.has_errors !== nextStep.has_errors
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -106,12 +107,7 @@ export default function useFunnelConfiguration({
|
|||||||
[funnel.funnel_id, selectedTime],
|
[funnel.funnel_id, selectedTime],
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check if all steps have both service_name and span_name defined
|
if (hasStepsChanged() && !hasIncompleteStepFields) {
|
||||||
const shouldUpdate = debouncedSteps.every(
|
|
||||||
(step) => step.service_name !== '' && step.span_name !== '',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasStepsChanged() && shouldUpdate) {
|
|
||||||
updateStepsMutation.mutate(getUpdatePayload(), {
|
updateStepsMutation.mutate(getUpdatePayload(), {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
const updatedFunnelSteps = data?.payload?.steps;
|
const updatedFunnelSteps = data?.payload?.steps;
|
||||||
@ -135,17 +131,10 @@ export default function useFunnelConfiguration({
|
|||||||
(step) => step.service_name === '' || step.span_name === '',
|
(step) => step.service_name === '' || step.span_name === '',
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasAllEmptyStepsData = updatedFunnelSteps.every(
|
|
||||||
(step) => step.service_name === '' && step.span_name === '',
|
|
||||||
);
|
|
||||||
|
|
||||||
setHasIncompleteStepFields(hasIncompleteStepFields);
|
|
||||||
setHasAllEmptyStepFields(hasAllEmptyStepsData);
|
|
||||||
|
|
||||||
// Only validate if service_name or span_name changed
|
// Only validate if service_name or span_name changed
|
||||||
if (
|
if (
|
||||||
!hasIncompleteStepFields &&
|
!hasIncompleteStepFields &&
|
||||||
hasStepServiceOrSpanNameChanged(lastValidatedSteps, debouncedSteps)
|
hasFunnelStepDefinitionsChanged(lastValidatedSteps, debouncedSteps)
|
||||||
) {
|
) {
|
||||||
queryClient.refetchQueries(validateStepsQueryKey);
|
queryClient.refetchQueries(validateStepsQueryKey);
|
||||||
setLastValidatedSteps(debouncedSteps);
|
setLastValidatedSteps(debouncedSteps);
|
||||||
@ -171,7 +160,7 @@ export default function useFunnelConfiguration({
|
|||||||
}, [
|
}, [
|
||||||
debouncedSteps,
|
debouncedSteps,
|
||||||
getUpdatePayload,
|
getUpdatePayload,
|
||||||
hasStepServiceOrSpanNameChanged,
|
hasFunnelStepDefinitionsChanged,
|
||||||
hasStepsChanged,
|
hasStepsChanged,
|
||||||
lastValidatedSteps,
|
lastValidatedSteps,
|
||||||
queryClient,
|
queryClient,
|
||||||
|
@ -2,8 +2,9 @@ import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
|||||||
import { MetricItem } from 'pages/TracesFunnelDetails/components/FunnelResults/FunnelMetricsTable';
|
import { MetricItem } from 'pages/TracesFunnelDetails/components/FunnelResults/FunnelMetricsTable';
|
||||||
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { LatencyOptions } from 'types/api/traceFunnels';
|
||||||
|
|
||||||
import { useFunnelOverview } from './useFunnels';
|
import { useFunnelOverview, useFunnelStepsOverview } from './useFunnels';
|
||||||
|
|
||||||
interface FunnelMetricsParams {
|
interface FunnelMetricsParams {
|
||||||
funnelId: string;
|
funnelId: string;
|
||||||
@ -13,8 +14,6 @@ interface FunnelMetricsParams {
|
|||||||
|
|
||||||
export function useFunnelMetrics({
|
export function useFunnelMetrics({
|
||||||
funnelId,
|
funnelId,
|
||||||
stepStart,
|
|
||||||
stepEnd,
|
|
||||||
}: FunnelMetricsParams): {
|
}: FunnelMetricsParams): {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isError: boolean;
|
isError: boolean;
|
||||||
@ -25,8 +24,6 @@ export function useFunnelMetrics({
|
|||||||
const payload = {
|
const payload = {
|
||||||
start_time: startTime,
|
start_time: startTime,
|
||||||
end_time: endTime,
|
end_time: endTime,
|
||||||
...(stepStart !== undefined && { step_start: stepStart }),
|
|
||||||
...(stepEnd !== undefined && { step_end: stepEnd }),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -48,14 +45,18 @@ export function useFunnelMetrics({
|
|||||||
{ title: 'Errors', value: sourceData.errors },
|
{ title: 'Errors', value: sourceData.errors },
|
||||||
{
|
{
|
||||||
title: 'Avg. Duration',
|
title: 'Avg. Duration',
|
||||||
value: getYAxisFormattedValue(sourceData.avg_duration.toString(), 'ns'),
|
value: getYAxisFormattedValue(sourceData.avg_duration.toString(), 'ms'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'P99 Latency',
|
title: `P99 Latency`,
|
||||||
value: getYAxisFormattedValue(sourceData.p99_latency.toString(), 'ns'),
|
value: getYAxisFormattedValue(
|
||||||
|
// TODO(shaheer): remove p99_latency once we have support for latency
|
||||||
|
(sourceData.latency ?? sourceData.p99_latency).toString(),
|
||||||
|
'ms',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [overviewData]);
|
}, [overviewData?.payload?.data]);
|
||||||
|
|
||||||
const conversionRate =
|
const conversionRate =
|
||||||
overviewData?.payload?.data?.[0]?.data?.conversion_rate ?? 0;
|
overviewData?.payload?.data?.[0]?.data?.conversion_rate ?? 0;
|
||||||
@ -67,3 +68,72 @@ export function useFunnelMetrics({
|
|||||||
conversionRate,
|
conversionRate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
export function useFunnelStepsMetrics({
|
||||||
|
funnelId,
|
||||||
|
stepStart,
|
||||||
|
stepEnd,
|
||||||
|
}: FunnelMetricsParams): {
|
||||||
|
isLoading: boolean;
|
||||||
|
isError: boolean;
|
||||||
|
metricsData: MetricItem[];
|
||||||
|
conversionRate: number;
|
||||||
|
} {
|
||||||
|
const { startTime, endTime, steps } = useFunnelContext();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
start_time: startTime,
|
||||||
|
end_time: endTime,
|
||||||
|
step_start: stepStart,
|
||||||
|
step_end: stepEnd,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: stepsOverviewData,
|
||||||
|
isLoading,
|
||||||
|
isFetching,
|
||||||
|
isError,
|
||||||
|
} = useFunnelStepsOverview(funnelId, payload);
|
||||||
|
|
||||||
|
const latencyType = useMemo(
|
||||||
|
() => (stepStart ? steps[stepStart]?.latency_type : LatencyOptions.P99),
|
||||||
|
[stepStart, steps],
|
||||||
|
);
|
||||||
|
|
||||||
|
const metricsData = useMemo(() => {
|
||||||
|
const sourceData = stepsOverviewData?.payload?.data?.[0]?.data;
|
||||||
|
if (!sourceData) return [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: 'Avg. Rate',
|
||||||
|
value: `${Number(sourceData.avg_rate.toFixed(2))} req/s`,
|
||||||
|
},
|
||||||
|
{ title: 'Errors', value: sourceData.errors },
|
||||||
|
{
|
||||||
|
title: 'Avg. Duration',
|
||||||
|
value: getYAxisFormattedValue(
|
||||||
|
(sourceData.avg_duration * 1_000_000).toString(),
|
||||||
|
'ns',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: `${latencyType?.toUpperCase()} Latency`,
|
||||||
|
value: getYAxisFormattedValue(
|
||||||
|
// TODO(shaheer): remove p99_latency once we have support for latency
|
||||||
|
((sourceData.latency ?? sourceData.p99_latency) * 1_000_000).toString(),
|
||||||
|
'ns',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [stepsOverviewData, latencyType]);
|
||||||
|
|
||||||
|
const conversionRate =
|
||||||
|
stepsOverviewData?.payload?.data?.[0]?.data?.conversion_rate ?? 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading: isLoading || isFetching,
|
||||||
|
isError,
|
||||||
|
metricsData,
|
||||||
|
conversionRate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -3,9 +3,10 @@ import {
|
|||||||
createFunnel,
|
createFunnel,
|
||||||
deleteFunnel,
|
deleteFunnel,
|
||||||
ErrorTraceData,
|
ErrorTraceData,
|
||||||
ErrorTracesPayload,
|
|
||||||
FunnelOverviewPayload,
|
FunnelOverviewPayload,
|
||||||
FunnelOverviewResponse,
|
FunnelOverviewResponse,
|
||||||
|
FunnelStepsOverviewPayload,
|
||||||
|
FunnelStepsOverviewResponse,
|
||||||
FunnelStepsResponse,
|
FunnelStepsResponse,
|
||||||
getFunnelById,
|
getFunnelById,
|
||||||
getFunnelErrorTraces,
|
getFunnelErrorTraces,
|
||||||
@ -13,11 +14,11 @@ import {
|
|||||||
getFunnelsList,
|
getFunnelsList,
|
||||||
getFunnelSlowTraces,
|
getFunnelSlowTraces,
|
||||||
getFunnelSteps,
|
getFunnelSteps,
|
||||||
|
getFunnelStepsOverview,
|
||||||
renameFunnel,
|
renameFunnel,
|
||||||
RenameFunnelPayload,
|
RenameFunnelPayload,
|
||||||
saveFunnelDescription,
|
saveFunnelDescription,
|
||||||
SlowTraceData,
|
SlowTraceData,
|
||||||
SlowTracesPayload,
|
|
||||||
updateFunnelSteps,
|
updateFunnelSteps,
|
||||||
UpdateFunnelStepsPayload,
|
UpdateFunnelStepsPayload,
|
||||||
ValidateFunnelResponse,
|
ValidateFunnelResponse,
|
||||||
@ -115,11 +116,13 @@ export const useValidateFunnelSteps = ({
|
|||||||
selectedTime,
|
selectedTime,
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
|
enabled,
|
||||||
}: {
|
}: {
|
||||||
funnelId: string;
|
funnelId: string;
|
||||||
selectedTime: string;
|
selectedTime: string;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
endTime: number;
|
endTime: number;
|
||||||
|
enabled: boolean;
|
||||||
}): UseQueryResult<
|
}): UseQueryResult<
|
||||||
SuccessResponse<ValidateFunnelResponse> | ErrorResponse,
|
SuccessResponse<ValidateFunnelResponse> | ErrorResponse,
|
||||||
Error
|
Error
|
||||||
@ -132,8 +135,8 @@ export const useValidateFunnelSteps = ({
|
|||||||
signal,
|
signal,
|
||||||
),
|
),
|
||||||
queryKey: [REACT_QUERY_KEY.VALIDATE_FUNNEL_STEPS, funnelId, selectedTime],
|
queryKey: [REACT_QUERY_KEY.VALIDATE_FUNNEL_STEPS, funnelId, selectedTime],
|
||||||
enabled: !!funnelId && !!selectedTime && !!startTime && !!endTime,
|
enabled,
|
||||||
staleTime: 1000 * 60 * 5,
|
staleTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
interface SaveFunnelDescriptionPayload {
|
interface SaveFunnelDescriptionPayload {
|
||||||
@ -157,7 +160,11 @@ export const useFunnelOverview = (
|
|||||||
SuccessResponse<FunnelOverviewResponse> | ErrorResponse,
|
SuccessResponse<FunnelOverviewResponse> | ErrorResponse,
|
||||||
Error
|
Error
|
||||||
> => {
|
> => {
|
||||||
const { selectedTime, validTracesCount } = useFunnelContext();
|
const {
|
||||||
|
selectedTime,
|
||||||
|
validTracesCount,
|
||||||
|
hasFunnelBeenExecuted,
|
||||||
|
} = useFunnelContext();
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryFn: ({ signal }) => getFunnelOverview(funnelId, payload, signal),
|
queryFn: ({ signal }) => getFunnelOverview(funnelId, payload, signal),
|
||||||
queryKey: [
|
queryKey: [
|
||||||
@ -167,31 +174,51 @@ export const useFunnelOverview = (
|
|||||||
payload.step_start ?? '',
|
payload.step_start ?? '',
|
||||||
payload.step_end ?? '',
|
payload.step_end ?? '',
|
||||||
],
|
],
|
||||||
enabled: !!funnelId && validTracesCount > 0,
|
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFunnelSlowTraces = (
|
export const useFunnelSlowTraces = (
|
||||||
funnelId: string,
|
funnelId: string,
|
||||||
payload: SlowTracesPayload,
|
payload: FunnelOverviewPayload,
|
||||||
): UseQueryResult<SuccessResponse<SlowTraceData> | ErrorResponse, Error> => {
|
): UseQueryResult<SuccessResponse<SlowTraceData> | ErrorResponse, Error> => {
|
||||||
const { selectedTime, validTracesCount } = useFunnelContext();
|
const {
|
||||||
|
selectedTime,
|
||||||
|
validTracesCount,
|
||||||
|
hasFunnelBeenExecuted,
|
||||||
|
} = useFunnelContext();
|
||||||
return useQuery<SuccessResponse<SlowTraceData> | ErrorResponse, Error>({
|
return useQuery<SuccessResponse<SlowTraceData> | ErrorResponse, Error>({
|
||||||
queryFn: ({ signal }) => getFunnelSlowTraces(funnelId, payload, signal),
|
queryFn: ({ signal }) => getFunnelSlowTraces(funnelId, payload, signal),
|
||||||
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_SLOW_TRACES, funnelId, selectedTime],
|
queryKey: [
|
||||||
enabled: !!funnelId && validTracesCount > 0,
|
REACT_QUERY_KEY.GET_FUNNEL_SLOW_TRACES,
|
||||||
|
funnelId,
|
||||||
|
selectedTime,
|
||||||
|
payload.step_start ?? '',
|
||||||
|
payload.step_end ?? '',
|
||||||
|
],
|
||||||
|
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFunnelErrorTraces = (
|
export const useFunnelErrorTraces = (
|
||||||
funnelId: string,
|
funnelId: string,
|
||||||
payload: ErrorTracesPayload,
|
payload: FunnelOverviewPayload,
|
||||||
): UseQueryResult<SuccessResponse<ErrorTraceData> | ErrorResponse, Error> => {
|
): UseQueryResult<SuccessResponse<ErrorTraceData> | ErrorResponse, Error> => {
|
||||||
const { selectedTime, validTracesCount } = useFunnelContext();
|
const {
|
||||||
|
selectedTime,
|
||||||
|
validTracesCount,
|
||||||
|
hasFunnelBeenExecuted,
|
||||||
|
} = useFunnelContext();
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryFn: ({ signal }) => getFunnelErrorTraces(funnelId, payload, signal),
|
queryFn: ({ signal }) => getFunnelErrorTraces(funnelId, payload, signal),
|
||||||
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_ERROR_TRACES, funnelId, selectedTime],
|
queryKey: [
|
||||||
enabled: !!funnelId && validTracesCount > 0,
|
REACT_QUERY_KEY.GET_FUNNEL_ERROR_TRACES,
|
||||||
|
funnelId,
|
||||||
|
selectedTime,
|
||||||
|
payload.step_start ?? '',
|
||||||
|
payload.step_end ?? '',
|
||||||
|
],
|
||||||
|
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -203,6 +230,7 @@ export function useFunnelStepsGraphData(
|
|||||||
endTime,
|
endTime,
|
||||||
selectedTime,
|
selectedTime,
|
||||||
validTracesCount,
|
validTracesCount,
|
||||||
|
hasFunnelBeenExecuted,
|
||||||
} = useFunnelContext();
|
} = useFunnelContext();
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@ -217,6 +245,31 @@ export function useFunnelStepsGraphData(
|
|||||||
funnelId,
|
funnelId,
|
||||||
selectedTime,
|
selectedTime,
|
||||||
],
|
],
|
||||||
enabled: !!funnelId && validTracesCount > 0,
|
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useFunnelStepsOverview = (
|
||||||
|
funnelId: string,
|
||||||
|
payload: FunnelStepsOverviewPayload,
|
||||||
|
): UseQueryResult<
|
||||||
|
SuccessResponse<FunnelStepsOverviewResponse> | ErrorResponse,
|
||||||
|
Error
|
||||||
|
> => {
|
||||||
|
const {
|
||||||
|
selectedTime,
|
||||||
|
validTracesCount,
|
||||||
|
hasFunnelBeenExecuted,
|
||||||
|
} = useFunnelContext();
|
||||||
|
return useQuery({
|
||||||
|
queryFn: ({ signal }) => getFunnelStepsOverview(funnelId, payload, signal),
|
||||||
|
queryKey: [
|
||||||
|
REACT_QUERY_KEY.GET_FUNNEL_STEPS_OVERVIEW,
|
||||||
|
funnelId,
|
||||||
|
selectedTime,
|
||||||
|
payload.step_start ?? '',
|
||||||
|
payload.step_end ?? '',
|
||||||
|
],
|
||||||
|
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
83
frontend/src/hooks/useLocalStorage.ts
Normal file
83
frontend/src/hooks/useLocalStorage.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A React hook for interacting with localStorage.
|
||||||
|
* It allows getting, setting, and removing items from localStorage.
|
||||||
|
*
|
||||||
|
* @template T The type of the value to be stored.
|
||||||
|
* @param {string} key The localStorage key.
|
||||||
|
* @param {T | (() => T)} initialValue The initial value to use if no value is found in localStorage,
|
||||||
|
* @returns {[T, (value: T | ((prevState: T) => T)) => void, () => void]}
|
||||||
|
* A tuple containing:
|
||||||
|
* - The current value from state (and localStorage).
|
||||||
|
* - A function to set the value (updates state and localStorage).
|
||||||
|
* - A function to remove the value from localStorage and reset state to initialValue.
|
||||||
|
*/
|
||||||
|
export function useLocalStorage<T>(
|
||||||
|
key: string,
|
||||||
|
initialValue: T | (() => T),
|
||||||
|
): [T, (value: T | ((prevState: T) => T)) => void, () => void] {
|
||||||
|
// This function resolves the initialValue if it's a function,
|
||||||
|
// and handles potential errors during localStorage access or JSON parsing.
|
||||||
|
const readValueFromStorage = useCallback((): T => {
|
||||||
|
const resolvedInitialValue =
|
||||||
|
initialValue instanceof Function ? initialValue() : initialValue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const item = window.localStorage.getItem(key);
|
||||||
|
// If item exists, parse it, otherwise return the resolved initial value.
|
||||||
|
if (item) {
|
||||||
|
return JSON.parse(item) as T;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Log error and fall back to initial value if reading/parsing fails.
|
||||||
|
console.warn(`Error reading localStorage key "${key}":`, error);
|
||||||
|
}
|
||||||
|
return resolvedInitialValue;
|
||||||
|
}, [key, initialValue]);
|
||||||
|
|
||||||
|
// Initialize state by reading from localStorage.
|
||||||
|
const [storedValue, setStoredValue] = useState<T>(readValueFromStorage);
|
||||||
|
|
||||||
|
// This function updates both localStorage and the React state.
|
||||||
|
const setValue = useCallback(
|
||||||
|
(value: T | ((prevState: T) => T)) => {
|
||||||
|
try {
|
||||||
|
// If a function is passed to setValue, it receives the latest value from storage.
|
||||||
|
const latestValueFromStorage = readValueFromStorage();
|
||||||
|
const valueToStore =
|
||||||
|
value instanceof Function ? value(latestValueFromStorage) : value;
|
||||||
|
|
||||||
|
// Save to localStorage.
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||||
|
// Update React state.
|
||||||
|
setStoredValue(valueToStore);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Error setting localStorage key "${key}":`, error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[key, readValueFromStorage],
|
||||||
|
);
|
||||||
|
|
||||||
|
// This function removes the item from localStorage and resets the React state.
|
||||||
|
const removeValue = useCallback(() => {
|
||||||
|
try {
|
||||||
|
window.localStorage.removeItem(key);
|
||||||
|
// Reset state to the (potentially resolved) initialValue.
|
||||||
|
setStoredValue(
|
||||||
|
initialValue instanceof Function ? initialValue() : initialValue,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Error removing localStorage key "${key}":`, error);
|
||||||
|
}
|
||||||
|
}, [key, initialValue]);
|
||||||
|
|
||||||
|
// useEffect to update the storedValue if the key changes,
|
||||||
|
// or if the initialValue prop changes causing readValueFromStorage to change.
|
||||||
|
// This ensures the hook reflects the correct localStorage item if its key prop dynamically changes.
|
||||||
|
useEffect(() => {
|
||||||
|
setStoredValue(readValueFromStorage());
|
||||||
|
}, [key, readValueFromStorage]); // Re-run if key or the read function changes.
|
||||||
|
|
||||||
|
return [storedValue, setValue, removeValue];
|
||||||
|
}
|
@ -39,7 +39,7 @@ function AddFunnelStepDetailsModal({
|
|||||||
setStepName(stepData?.name || '');
|
setStepName(stepData?.name || '');
|
||||||
setDescription(stepData?.description || '');
|
setDescription(stepData?.description || '');
|
||||||
}
|
}
|
||||||
}, [isOpen, stepData]);
|
}, [isOpen, stepData?.name, stepData?.description]);
|
||||||
|
|
||||||
const handleCancel = (): void => {
|
const handleCancel = (): void => {
|
||||||
setStepName('');
|
setStepName('');
|
||||||
|
@ -26,7 +26,7 @@ function InterStepConfig({
|
|||||||
</div>
|
</div>
|
||||||
<div className="inter-step-config__latency-options">
|
<div className="inter-step-config__latency-options">
|
||||||
<SignozRadioGroup
|
<SignozRadioGroup
|
||||||
value={step.latency_type}
|
value={step.latency_type ?? LatencyOptions.P99}
|
||||||
options={options}
|
options={options}
|
||||||
onChange={(e): void =>
|
onChange={(e): void =>
|
||||||
onStepChange(index, {
|
onStepChange(index, {
|
||||||
|
@ -65,7 +65,8 @@ function StepsContent({
|
|||||||
</div>
|
</div>
|
||||||
{/* Display InterStepConfig only between steps */}
|
{/* Display InterStepConfig only between steps */}
|
||||||
{index < steps.length - 1 && (
|
{index < steps.length - 1 && (
|
||||||
<InterStepConfig index={index} step={step} />
|
// the latency type should be sent with the n+1th step
|
||||||
|
<InterStepConfig index={index + 1} step={steps[index + 1]} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -34,12 +34,16 @@
|
|||||||
border: none;
|
border: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
font-size: 12px;
|
||||||
.ant-btn-icon {
|
font-weight: 500;
|
||||||
margin-inline-end: 0 !important;
|
line-height: 10px; /* 83.333% */
|
||||||
}
|
letter-spacing: 0.12px;
|
||||||
&--save {
|
border-radius: 2px;
|
||||||
background-color: var(--bg-slate-400);
|
|
||||||
|
&--sync {
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
}
|
}
|
||||||
&--run {
|
&--run {
|
||||||
background-color: var(--bg-robin-500);
|
background-color: var(--bg-robin-500);
|
||||||
|
@ -1,8 +1,49 @@
|
|||||||
import './StepsFooter.styles.scss';
|
import './StepsFooter.styles.scss';
|
||||||
|
|
||||||
import { Button, Skeleton } from 'antd';
|
import { Button, Skeleton } from 'antd';
|
||||||
import { Cone, Play } from 'lucide-react';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import { Cone, Play, RefreshCcw } from 'lucide-react';
|
||||||
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { useIsFetching, useQueryClient } from 'react-query';
|
||||||
|
|
||||||
|
const useFunnelResultsLoading = (): boolean => {
|
||||||
|
const { funnelId } = useFunnelContext();
|
||||||
|
|
||||||
|
const isFetchingFunnelOverview = useIsFetching({
|
||||||
|
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_OVERVIEW, funnelId],
|
||||||
|
});
|
||||||
|
|
||||||
|
const isFetchingStepsGraphData = useIsFetching({
|
||||||
|
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_STEPS_GRAPH_DATA, funnelId],
|
||||||
|
});
|
||||||
|
|
||||||
|
const isFetchingErrorTraces = useIsFetching({
|
||||||
|
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_ERROR_TRACES, funnelId],
|
||||||
|
});
|
||||||
|
|
||||||
|
const isFetchingSlowTraces = useIsFetching({
|
||||||
|
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_SLOW_TRACES, funnelId],
|
||||||
|
});
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!funnelId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
!!isFetchingFunnelOverview ||
|
||||||
|
!!isFetchingStepsGraphData ||
|
||||||
|
!!isFetchingErrorTraces ||
|
||||||
|
!!isFetchingSlowTraces
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
funnelId,
|
||||||
|
isFetchingFunnelOverview,
|
||||||
|
isFetchingStepsGraphData,
|
||||||
|
isFetchingErrorTraces,
|
||||||
|
isFetchingSlowTraces,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
interface StepsFooterProps {
|
interface StepsFooterProps {
|
||||||
stepsCount: number;
|
stepsCount: number;
|
||||||
@ -14,10 +55,27 @@ function ValidTracesCount(): JSX.Element {
|
|||||||
isValidateStepsLoading,
|
isValidateStepsLoading,
|
||||||
hasIncompleteStepFields,
|
hasIncompleteStepFields,
|
||||||
validTracesCount,
|
validTracesCount,
|
||||||
|
funnelId,
|
||||||
|
selectedTime,
|
||||||
} = useFunnelContext();
|
} = useFunnelContext();
|
||||||
if (isValidateStepsLoading) {
|
const queryClient = useQueryClient();
|
||||||
return <Skeleton.Button size="small" />;
|
const validationQueryKey = useMemo(
|
||||||
|
() => [REACT_QUERY_KEY.VALIDATE_FUNNEL_STEPS, funnelId, selectedTime],
|
||||||
|
[funnelId, selectedTime],
|
||||||
|
);
|
||||||
|
const validationStatus = queryClient.getQueryData(validationQueryKey);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Show loading state immediately when fields become valid
|
||||||
|
if (hasIncompleteStepFields && validationStatus !== 'pending') {
|
||||||
|
queryClient.setQueryData(validationQueryKey, 'pending');
|
||||||
}
|
}
|
||||||
|
}, [
|
||||||
|
hasIncompleteStepFields,
|
||||||
|
queryClient,
|
||||||
|
validationQueryKey,
|
||||||
|
validationStatus,
|
||||||
|
]);
|
||||||
|
|
||||||
if (hasAllEmptyStepFields) {
|
if (hasAllEmptyStepFields) {
|
||||||
return (
|
return (
|
||||||
@ -33,6 +91,10 @@ function ValidTracesCount(): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isValidateStepsLoading || validationStatus === 'pending') {
|
||||||
|
return <Skeleton.Button size="small" />;
|
||||||
|
}
|
||||||
|
|
||||||
if (validTracesCount === 0) {
|
if (validTracesCount === 0) {
|
||||||
return (
|
return (
|
||||||
<span className="steps-footer__valid-traces steps-footer__valid-traces--none">
|
<span className="steps-footer__valid-traces steps-footer__valid-traces--none">
|
||||||
@ -45,7 +107,13 @@ function ValidTracesCount(): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function StepsFooter({ stepsCount }: StepsFooterProps): JSX.Element {
|
function StepsFooter({ stepsCount }: StepsFooterProps): JSX.Element {
|
||||||
const { validTracesCount, handleRunFunnel } = useFunnelContext();
|
const {
|
||||||
|
validTracesCount,
|
||||||
|
handleRunFunnel,
|
||||||
|
hasFunnelBeenExecuted,
|
||||||
|
} = useFunnelContext();
|
||||||
|
|
||||||
|
const isFunnelResultsLoading = useFunnelResultsLoading();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="steps-footer">
|
<div className="steps-footer">
|
||||||
@ -56,6 +124,7 @@ function StepsFooter({ stepsCount }: StepsFooterProps): JSX.Element {
|
|||||||
<ValidTracesCount />
|
<ValidTracesCount />
|
||||||
</div>
|
</div>
|
||||||
<div className="steps-footer__right">
|
<div className="steps-footer__right">
|
||||||
|
{!hasFunnelBeenExecuted ? (
|
||||||
<Button
|
<Button
|
||||||
disabled={validTracesCount === 0}
|
disabled={validTracesCount === 0}
|
||||||
onClick={handleRunFunnel}
|
onClick={handleRunFunnel}
|
||||||
@ -65,6 +134,18 @@ function StepsFooter({ stepsCount }: StepsFooterProps): JSX.Element {
|
|||||||
>
|
>
|
||||||
Run funnel
|
Run funnel
|
||||||
</Button>
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
className="steps-footer__button steps-footer__button--sync"
|
||||||
|
icon={<RefreshCcw size={16} />}
|
||||||
|
onClick={handleRunFunnel}
|
||||||
|
loading={isFunnelResultsLoading}
|
||||||
|
disabled={validTracesCount === 0}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -18,7 +18,7 @@ function EmptyFunnelResults({
|
|||||||
<div className="empty-funnel-results__title">{title}</div>
|
<div className="empty-funnel-results__title">{title}</div>
|
||||||
<div className="empty-funnel-results__description">{description}</div>
|
<div className="empty-funnel-results__description">{description}</div>
|
||||||
<div className="empty-funnel-results__learn-more">
|
<div className="empty-funnel-results__learn-more">
|
||||||
<LearnMore />
|
<LearnMore url="https://signoz.io/blog/tracing-funnels-observability-distributed-systems/" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import './FunnelResults.styles.scss';
|
import './FunnelResults.styles.scss';
|
||||||
|
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
||||||
|
import { useQueryClient } from 'react-query';
|
||||||
|
|
||||||
import EmptyFunnelResults from './EmptyFunnelResults';
|
import EmptyFunnelResults from './EmptyFunnelResults';
|
||||||
import FunnelGraph from './FunnelGraph';
|
import FunnelGraph from './FunnelGraph';
|
||||||
@ -14,11 +16,17 @@ function FunnelResults(): JSX.Element {
|
|||||||
isValidateStepsLoading,
|
isValidateStepsLoading,
|
||||||
hasIncompleteStepFields,
|
hasIncompleteStepFields,
|
||||||
hasAllEmptyStepFields,
|
hasAllEmptyStepFields,
|
||||||
|
hasFunnelBeenExecuted,
|
||||||
|
funnelId,
|
||||||
|
selectedTime,
|
||||||
} = useFunnelContext();
|
} = useFunnelContext();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
if (isValidateStepsLoading) {
|
const validateQueryData = queryClient.getQueryData([
|
||||||
return <Spinner size="large" />;
|
REACT_QUERY_KEY.VALIDATE_FUNNEL_STEPS,
|
||||||
}
|
funnelId,
|
||||||
|
selectedTime,
|
||||||
|
]);
|
||||||
|
|
||||||
if (hasAllEmptyStepFields) return <EmptyFunnelResults />;
|
if (hasAllEmptyStepFields) return <EmptyFunnelResults />;
|
||||||
|
|
||||||
@ -30,6 +38,10 @@ function FunnelResults(): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isValidateStepsLoading || validateQueryData === 'pending') {
|
||||||
|
return <Spinner size="large" />;
|
||||||
|
}
|
||||||
|
|
||||||
if (validTracesCount === 0) {
|
if (validTracesCount === 0) {
|
||||||
return (
|
return (
|
||||||
<EmptyFunnelResults
|
<EmptyFunnelResults
|
||||||
@ -38,6 +50,14 @@ function FunnelResults(): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (!hasFunnelBeenExecuted) {
|
||||||
|
return (
|
||||||
|
<EmptyFunnelResults
|
||||||
|
title="Funnel has not been run yet."
|
||||||
|
description="Run the funnel to see the results"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="funnel-results">
|
<div className="funnel-results">
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import { ErrorTraceData, SlowTraceData } from 'api/traceFunnels';
|
import {
|
||||||
|
ErrorTraceData,
|
||||||
|
FunnelOverviewPayload,
|
||||||
|
SlowTraceData,
|
||||||
|
} from 'api/traceFunnels';
|
||||||
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } from 'react-query';
|
||||||
@ -15,12 +19,7 @@ interface FunnelTopTracesTableProps {
|
|||||||
tooltip: string;
|
tooltip: string;
|
||||||
useQueryHook: (
|
useQueryHook: (
|
||||||
funnelId: string,
|
funnelId: string,
|
||||||
payload: {
|
payload: FunnelOverviewPayload,
|
||||||
start_time: number;
|
|
||||||
end_time: number;
|
|
||||||
step_a_order: number;
|
|
||||||
step_b_order: number;
|
|
||||||
},
|
|
||||||
) => UseQueryResult<
|
) => UseQueryResult<
|
||||||
SuccessResponse<SlowTraceData | ErrorTraceData> | ErrorResponse,
|
SuccessResponse<SlowTraceData | ErrorTraceData> | ErrorResponse,
|
||||||
Error
|
Error
|
||||||
@ -40,8 +39,8 @@ function FunnelTopTracesTable({
|
|||||||
() => ({
|
() => ({
|
||||||
start_time: startTime,
|
start_time: startTime,
|
||||||
end_time: endTime,
|
end_time: endTime,
|
||||||
step_a_order: stepAOrder,
|
step_start: stepAOrder,
|
||||||
step_b_order: stepBOrder,
|
step_end: stepBOrder,
|
||||||
}),
|
}),
|
||||||
[startTime, endTime, stepAOrder, stepBOrder],
|
[startTime, endTime, stepAOrder, stepBOrder],
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useFunnelMetrics } from 'hooks/TracesFunnels/useFunnelMetrics';
|
import { useFunnelStepsMetrics } from 'hooks/TracesFunnels/useFunnelMetrics';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import FunnelMetricsTable from './FunnelMetricsTable';
|
import FunnelMetricsTable from './FunnelMetricsTable';
|
||||||
@ -22,7 +22,7 @@ function StepsTransitionMetrics({
|
|||||||
(transition) => transition.value === selectedTransition,
|
(transition) => transition.value === selectedTransition,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isLoading, metricsData, conversionRate } = useFunnelMetrics({
|
const { isLoading, metricsData, conversionRate } = useFunnelStepsMetrics({
|
||||||
funnelId: funnelId || '',
|
funnelId: funnelId || '',
|
||||||
stepStart: startStep,
|
stepStart: startStep,
|
||||||
stepEnd: endStep,
|
stepEnd: endStep,
|
||||||
|
@ -13,7 +13,7 @@ export const topTracesTableColumns = [
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'DURATION',
|
title: 'STEP TRANSITION DURATION',
|
||||||
dataIndex: 'duration_ms',
|
dataIndex: 'duration_ms',
|
||||||
key: 'duration_ms',
|
key: 'duration_ms',
|
||||||
render: (value: string): string => getYAxisFormattedValue(value, 'ms'),
|
render: (value: string): string => getYAxisFormattedValue(value, 'ms'),
|
||||||
|
@ -12,7 +12,7 @@ export const initialStepsData: FunnelStepData[] = [
|
|||||||
op: 'and',
|
op: 'and',
|
||||||
},
|
},
|
||||||
latency_pointer: 'start',
|
latency_pointer: 'start',
|
||||||
latency_type: LatencyOptions.P95,
|
latency_type: undefined,
|
||||||
has_errors: false,
|
has_errors: false,
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { ValidateFunnelResponse } from 'api/traceFunnels';
|
import { ValidateFunnelResponse } from 'api/traceFunnels';
|
||||||
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||||
import {
|
import {
|
||||||
@ -7,6 +8,7 @@ import {
|
|||||||
Time as TimeV2,
|
Time as TimeV2,
|
||||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
import { useValidateFunnelSteps } from 'hooks/TracesFunnels/useFunnels';
|
import { useValidateFunnelSteps } from 'hooks/TracesFunnels/useFunnels';
|
||||||
|
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||||
import { initialStepsData } from 'pages/TracesFunnelDetails/constants';
|
import { initialStepsData } from 'pages/TracesFunnelDetails/constants';
|
||||||
import {
|
import {
|
||||||
@ -45,15 +47,15 @@ interface FunnelContextType {
|
|||||||
| undefined;
|
| undefined;
|
||||||
isValidateStepsLoading: boolean;
|
isValidateStepsLoading: boolean;
|
||||||
hasIncompleteStepFields: boolean;
|
hasIncompleteStepFields: boolean;
|
||||||
setHasIncompleteStepFields: Dispatch<SetStateAction<boolean>>;
|
|
||||||
hasAllEmptyStepFields: boolean;
|
hasAllEmptyStepFields: boolean;
|
||||||
setHasAllEmptyStepFields: Dispatch<SetStateAction<boolean>>;
|
|
||||||
handleReplaceStep: (
|
handleReplaceStep: (
|
||||||
index: number,
|
index: number,
|
||||||
serviceName: string,
|
serviceName: string,
|
||||||
spanName: string,
|
spanName: string,
|
||||||
) => void;
|
) => void;
|
||||||
handleRestoreSteps: (oldSteps: FunnelStepData[]) => void;
|
handleRestoreSteps: (oldSteps: FunnelStepData[]) => void;
|
||||||
|
hasFunnelBeenExecuted: boolean;
|
||||||
|
setHasFunnelBeenExecuted: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FunnelContext = createContext<FunnelContextType | undefined>(undefined);
|
const FunnelContext = createContext<FunnelContextType | undefined>(undefined);
|
||||||
@ -84,12 +86,27 @@ export function FunnelProvider({
|
|||||||
const funnel = data?.payload;
|
const funnel = data?.payload;
|
||||||
const initialSteps = funnel?.steps?.length ? funnel.steps : initialStepsData;
|
const initialSteps = funnel?.steps?.length ? funnel.steps : initialStepsData;
|
||||||
const [steps, setSteps] = useState<FunnelStepData[]>(initialSteps);
|
const [steps, setSteps] = useState<FunnelStepData[]>(initialSteps);
|
||||||
const [hasIncompleteStepFields, setHasIncompleteStepFields] = useState(
|
const { hasIncompleteStepFields, hasAllEmptyStepFields } = useMemo(
|
||||||
steps.some((step) => step.service_name === '' || step.span_name === ''),
|
() => ({
|
||||||
|
hasAllEmptyStepFields: steps.every(
|
||||||
|
(step) => step.service_name === '' && step.span_name === '',
|
||||||
|
),
|
||||||
|
hasIncompleteStepFields: steps.some(
|
||||||
|
(step) => step.service_name === '' || step.span_name === '',
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[steps],
|
||||||
);
|
);
|
||||||
const [hasAllEmptyStepFields, setHasAllEmptyStepFields] = useState(
|
|
||||||
steps.every((step) => step.service_name === '' && step.span_name === ''),
|
const [unexecutedFunnels, setUnexecutedFunnels] = useLocalStorage<string[]>(
|
||||||
|
LOCALSTORAGE.UNEXECUTED_FUNNELS,
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [hasFunnelBeenExecuted, setHasFunnelBeenExecuted] = useState(
|
||||||
|
!unexecutedFunnels.includes(funnelId),
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: validationResponse,
|
data: validationResponse,
|
||||||
isLoading: isValidationLoading,
|
isLoading: isValidationLoading,
|
||||||
@ -99,6 +116,7 @@ export function FunnelProvider({
|
|||||||
selectedTime,
|
selectedTime,
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
|
enabled: !!funnelId && !!selectedTime && !!startTime && !!endTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
const validTracesCount = useMemo(
|
const validTracesCount = useMemo(
|
||||||
@ -163,6 +181,11 @@ export function FunnelProvider({
|
|||||||
|
|
||||||
const handleRunFunnel = useCallback(async (): Promise<void> => {
|
const handleRunFunnel = useCallback(async (): Promise<void> => {
|
||||||
if (validTracesCount === 0) return;
|
if (validTracesCount === 0) return;
|
||||||
|
if (!hasFunnelBeenExecuted) {
|
||||||
|
setUnexecutedFunnels(unexecutedFunnels.filter((id) => id !== funnelId));
|
||||||
|
|
||||||
|
setHasFunnelBeenExecuted(true);
|
||||||
|
}
|
||||||
queryClient.refetchQueries([
|
queryClient.refetchQueries([
|
||||||
REACT_QUERY_KEY.GET_FUNNEL_OVERVIEW,
|
REACT_QUERY_KEY.GET_FUNNEL_OVERVIEW,
|
||||||
funnelId,
|
funnelId,
|
||||||
@ -183,7 +206,15 @@ export function FunnelProvider({
|
|||||||
funnelId,
|
funnelId,
|
||||||
selectedTime,
|
selectedTime,
|
||||||
]);
|
]);
|
||||||
}, [funnelId, queryClient, selectedTime, validTracesCount]);
|
}, [
|
||||||
|
funnelId,
|
||||||
|
hasFunnelBeenExecuted,
|
||||||
|
unexecutedFunnels,
|
||||||
|
queryClient,
|
||||||
|
selectedTime,
|
||||||
|
setUnexecutedFunnels,
|
||||||
|
validTracesCount,
|
||||||
|
]);
|
||||||
|
|
||||||
const value = useMemo<FunnelContextType>(
|
const value = useMemo<FunnelContextType>(
|
||||||
() => ({
|
() => ({
|
||||||
@ -202,11 +233,11 @@ export function FunnelProvider({
|
|||||||
validationResponse,
|
validationResponse,
|
||||||
isValidateStepsLoading: isValidationLoading || isValidationFetching,
|
isValidateStepsLoading: isValidationLoading || isValidationFetching,
|
||||||
hasIncompleteStepFields,
|
hasIncompleteStepFields,
|
||||||
setHasIncompleteStepFields,
|
|
||||||
hasAllEmptyStepFields,
|
hasAllEmptyStepFields,
|
||||||
setHasAllEmptyStepFields,
|
|
||||||
handleReplaceStep,
|
handleReplaceStep,
|
||||||
handleRestoreSteps,
|
handleRestoreSteps,
|
||||||
|
hasFunnelBeenExecuted,
|
||||||
|
setHasFunnelBeenExecuted,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
funnelId,
|
funnelId,
|
||||||
@ -224,11 +255,11 @@ export function FunnelProvider({
|
|||||||
isValidationLoading,
|
isValidationLoading,
|
||||||
isValidationFetching,
|
isValidationFetching,
|
||||||
hasIncompleteStepFields,
|
hasIncompleteStepFields,
|
||||||
setHasIncompleteStepFields,
|
|
||||||
hasAllEmptyStepFields,
|
hasAllEmptyStepFields,
|
||||||
setHasAllEmptyStepFields,
|
|
||||||
handleReplaceStep,
|
handleReplaceStep,
|
||||||
handleRestoreSteps,
|
handleRestoreSteps,
|
||||||
|
hasFunnelBeenExecuted,
|
||||||
|
setHasFunnelBeenExecuted,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -4,9 +4,11 @@ import { Input } from 'antd';
|
|||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import SignozModal from 'components/SignozModal/SignozModal';
|
import SignozModal from 'components/SignozModal/SignozModal';
|
||||||
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { useCreateFunnel } from 'hooks/TracesFunnels/useFunnels';
|
import { useCreateFunnel } from 'hooks/TracesFunnels/useFunnels';
|
||||||
|
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import { Check, X } from 'lucide-react';
|
import { Check, X } from 'lucide-react';
|
||||||
@ -32,6 +34,11 @@ function CreateFunnel({
|
|||||||
const { safeNavigate } = useSafeNavigate();
|
const { safeNavigate } = useSafeNavigate();
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const [unexecutedFunnels, setUnexecutedFunnels] = useLocalStorage<string[]>(
|
||||||
|
LOCALSTORAGE.UNEXECUTED_FUNNELS,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const handleCreate = (): void => {
|
const handleCreate = (): void => {
|
||||||
createFunnelMutation.mutate(
|
createFunnelMutation.mutate(
|
||||||
{
|
{
|
||||||
@ -52,11 +59,17 @@ function CreateFunnel({
|
|||||||
|
|
||||||
setFunnelName('');
|
setFunnelName('');
|
||||||
queryClient.invalidateQueries([REACT_QUERY_KEY.GET_FUNNELS_LIST]);
|
queryClient.invalidateQueries([REACT_QUERY_KEY.GET_FUNNELS_LIST]);
|
||||||
onClose(data?.payload?.funnel_id);
|
|
||||||
if (data?.payload?.funnel_id && redirectToDetails) {
|
const funnelId = data?.payload?.funnel_id;
|
||||||
|
if (funnelId) {
|
||||||
|
setUnexecutedFunnels([...unexecutedFunnels, funnelId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose(funnelId);
|
||||||
|
if (funnelId && redirectToDetails) {
|
||||||
safeNavigate(
|
safeNavigate(
|
||||||
generatePath(ROUTES.TRACES_FUNNELS_DETAIL, {
|
generatePath(ROUTES.TRACES_FUNNELS_DETAIL, {
|
||||||
funnelId: data.payload.funnel_id,
|
funnelId,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ function FunnelsEmptyState({
|
|||||||
>
|
>
|
||||||
New funnel
|
New funnel
|
||||||
</Button>
|
</Button>
|
||||||
<LearnMore />
|
<LearnMore url="https://signoz.io/blog/tracing-funnels-observability-distributed-systems/" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,6 +40,10 @@ function RenameFunnel({
|
|||||||
message: 'Funnel renamed successfully',
|
message: 'Funnel renamed successfully',
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries([REACT_QUERY_KEY.GET_FUNNELS_LIST]);
|
queryClient.invalidateQueries([REACT_QUERY_KEY.GET_FUNNELS_LIST]);
|
||||||
|
queryClient.invalidateQueries([
|
||||||
|
REACT_QUERY_KEY.GET_FUNNEL_DETAILS,
|
||||||
|
funnelId,
|
||||||
|
]);
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
|
@ -14,7 +14,7 @@ export interface FunnelStepData {
|
|||||||
span_name: string;
|
span_name: string;
|
||||||
filters: TagFilter;
|
filters: TagFilter;
|
||||||
latency_pointer: 'start' | 'end';
|
latency_pointer: 'start' | 'end';
|
||||||
latency_type: LatencyOptionsType;
|
latency_type?: LatencyOptionsType;
|
||||||
has_errors: boolean;
|
has_errors: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user