Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ee/query-service/app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.RegisterMessagingQueuesRoutes(r, am)
apiHandler.RegisterThirdPartyApiRoutes(r, am)
apiHandler.MetricExplorerRoutes(r, am)
apiHandler.RegisterTraceFunnelsRoutes(r, am)

c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
Expand Down
2 changes: 2 additions & 0 deletions frontend/public/Icons/empty-funnel-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/public/Icons/funnel-add.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/public/Icons/solid-info-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion frontend/src/AppRoutes/pageComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ export const TracesFunnels = Loadable(
import(/* webpackChunkName: "Traces Funnels" */ 'pages/TracesModulePage'),
);
export const TracesFunnelDetails = Loadable(
// eslint-disable-next-line sonarjs/no-identical-functions
() =>
import(
/* webpackChunkName: "Traces Funnel Details" */ 'pages/TracesFunnelDetails'
/* webpackChunkName: "Traces Funnel Details" */ 'pages/TracesModulePage'
),
);

Expand Down
267 changes: 266 additions & 1 deletion frontend/src/api/traceFunnels/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
CreateFunnelPayload,
CreateFunnelResponse,
FunnelData,
FunnelStepData,
} from 'types/api/traceFunnels';

const FUNNELS_BASE_PATH = '/trace-funnels';
Expand Down Expand Up @@ -54,7 +55,7 @@ export const getFunnelsList = async ({
};

export const getFunnelById = async (
funnelId: string,
funnelId?: string,
): Promise<SuccessResponse<FunnelData> | ErrorResponse> => {
const response: AxiosResponse = await axios.get(
`${FUNNELS_BASE_PATH}/get/${funnelId}`,
Expand Down Expand Up @@ -107,3 +108,267 @@ export const deleteFunnel = async (
payload: response.data,
};
};

export interface UpdateFunnelStepsPayload {
funnel_id: string;
steps: FunnelStepData[];
updated_timestamp: number;
}

export const updateFunnelSteps = async (
payload: UpdateFunnelStepsPayload,
): Promise<SuccessResponse<FunnelData> | ErrorResponse> => {
const response: AxiosResponse = await axios.put(
`${FUNNELS_BASE_PATH}/steps/update`,
payload,
);

return {
statusCode: 200,
error: null,
message: 'Funnel steps updated successfully',
payload: response.data,
};
};

export interface ValidateFunnelPayload {
start_time: number;
end_time: number;
}

export interface ValidateFunnelResponse {
status: string;
data: Array<{
timestamp: string;
data: {
trace_id: string;
};
}> | null;
}

export const validateFunnelSteps = async (
funnelId: string,
payload: ValidateFunnelPayload,
signal?: AbortSignal,
): Promise<SuccessResponse<ValidateFunnelResponse> | ErrorResponse> => {
const response = await axios.post(
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/validate`,
payload,
{ signal },
);

return {
statusCode: 200,
error: null,
message: '',
payload: response.data,
};
};

export interface UpdateFunnelStepDetailsPayload {
funnel_id: string;
steps: Array<{
step_name: string;
description: string;
}>;
updated_timestamp: number;
}

export const updateFunnelStepDetails = async ({
stepOrder,
payload,
}: {
stepOrder: number;
payload: UpdateFunnelStepDetailsPayload;
}): Promise<SuccessResponse<FunnelData> | ErrorResponse> => {
const response: AxiosResponse = await axios.put(
`${FUNNELS_BASE_PATH}/steps/${stepOrder}/update`,
payload,
);

return {
statusCode: 200,
error: null,
message: 'Funnel step details updated successfully',
payload: response.data,
};
};

interface UpdateFunnelDescriptionPayload {
funnel_id: string;
description: string;
}

export const saveFunnelDescription = async (
payload: UpdateFunnelDescriptionPayload,
): Promise<SuccessResponse<FunnelData> | ErrorResponse> => {
const response: AxiosResponse = await axios.post(
`${FUNNELS_BASE_PATH}/save`,
payload,
);

return {
statusCode: 200,
error: null,
message: 'Funnel description updated successfully',
payload: response.data,
};
};

export interface FunnelOverviewPayload {
start_time: number;
end_time: number;
step_start?: number;
step_end?: number;
}

export interface FunnelOverviewResponse {
status: string;
data: Array<{
timestamp: string;
data: {
avg_duration: number;
avg_rate: number;
conversion_rate: number | null;
errors: number;
p99_latency: number;
};
}>;
}

export const getFunnelOverview = async (
funnelId: string,
payload: FunnelOverviewPayload,
signal?: AbortSignal,
): Promise<SuccessResponse<FunnelOverviewResponse> | ErrorResponse> => {
const response = await axios.post(
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/overview`,
payload,
{
signal,
},
);

return {
statusCode: 200,
error: null,
message: '',
payload: response.data,
};
};

export interface SlowTracesPayload {
start_time: number;
end_time: number;
step_a_order: number;
step_b_order: number;
}

export interface SlowTraceData {
status: string;
data: Array<{
timestamp: string;
data: {
duration_ms: string;
span_count: number;
trace_id: string;
};
}>;
}

export const getFunnelSlowTraces = async (
funnelId: string,
payload: SlowTracesPayload,
signal?: AbortSignal,
): Promise<SuccessResponse<SlowTraceData> | ErrorResponse> => {
const response = await axios.post(
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/slow-traces`,
payload,
{
signal,
},
);

return {
statusCode: 200,
error: null,
message: '',
payload: response.data,
};
};
export interface ErrorTracesPayload {
start_time: number;
end_time: number;
step_a_order: number;
step_b_order: number;
}

export interface ErrorTraceData {
status: string;
data: Array<{
timestamp: string;
data: {
duration_ms: string;
span_count: number;
trace_id: string;
};
}>;
}

export const getFunnelErrorTraces = async (
funnelId: string,
payload: ErrorTracesPayload,
signal?: AbortSignal,
): Promise<SuccessResponse<ErrorTraceData> | ErrorResponse> => {
const response: AxiosResponse = await axios.post(
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/error-traces`,
payload,
{
signal,
},
);

return {
statusCode: 200,
error: null,
message: '',
payload: response.data,
};
};

export interface FunnelStepsPayload {
start_time: number;
end_time: number;
}

export interface FunnelStepGraphMetrics {
[key: `total_s${number}_spans`]: number;
[key: `total_s${number}_errored_spans`]: number;
}

export interface FunnelStepsResponse {
status: string;
data: Array<{
timestamp: string;
data: FunnelStepGraphMetrics;
}>;
}

export const getFunnelSteps = async (
funnelId: string,
payload: FunnelStepsPayload,
signal?: AbortSignal,
): Promise<SuccessResponse<FunnelStepsResponse> | ErrorResponse> => {
const response = await axios.post(
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/steps`,
payload,
{ signal },
);

return {
statusCode: 200,
error: null,
message: '',
payload: response.data,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,24 @@ import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { useHistory, useLocation } from 'react-router-dom';

interface SelectOptionConfig {
export interface SelectOptionConfig {
placeholder: string;
queryParam: QueryParams;
filterType: string | string[];
shouldSetQueryParams?: boolean;
onChange?: (value: string | string[]) => void;
values?: string | string[];
isMultiple?: boolean;
}

function FilterSelect({
export function FilterSelect({
placeholder,
queryParam,
filterType,
values,
shouldSetQueryParams,
onChange,
isMultiple,
}: SelectOptionConfig): JSX.Element {
const { handleSearch, isFetching, options } = useCeleryFilterOptions(
filterType,
Expand All @@ -35,15 +43,20 @@ function FilterSelect({
key={filterType.toString()}
placeholder={placeholder}
showSearch
mode="multiple"
// eslint-disable-next-line react/jsx-props-no-spreading
{...(isMultiple ? { mode: 'multiple' } : {})}
options={options}
loading={isFetching}
className="config-select-option"
onSearch={handleSearch}
maxTagCount={4}
allowClear
maxTagPlaceholder={SelectMaxTagPlaceholder}
value={getValuesFromQueryParams(queryParam, urlQuery) || []}
value={
!shouldSetQueryParams && !!values?.length
? values
: getValuesFromQueryParams(queryParam, urlQuery) || []
}
notFoundContent={
isFetching ? (
<span>
Expand All @@ -55,12 +68,28 @@ function FilterSelect({
}
onChange={(value): void => {
handleSearch('');
setQueryParamsFromOptions(value, urlQuery, history, location, queryParam);
if (shouldSetQueryParams) {
setQueryParamsFromOptions(
value as string[],
urlQuery,
history,
location,
queryParam,
);
}
onChange?.(value);
}}
/>
);
}

FilterSelect.defaultProps = {
shouldSetQueryParams: true,
onChange: (): void => {},
values: [],
isMultiple: true,
};

function CeleryOverviewConfigOptions(): JSX.Element {
const selectConfigs: SelectOptionConfig[] = [
{
Expand Down
Loading
Loading