element
width: table.getTotalSize(),
+ height: `${virtualizer.getTotalSize()}px`,
}}
>
@@ -113,6 +175,9 @@ export function TableV3(props: ITableV3Props): JSX.Element {
onDoubleClick: (): void => header.column.resetSize(),
onMouseDown: header.getResizeHandler(),
onTouchStart: header.getResizeHandler(),
+ style: {
+ display: !header.column.getCanResize() ? 'none' : '',
+ },
className: `resizer ${
header.column.getIsResizing() ? 'isResizing' : ''
}`,
@@ -125,11 +190,16 @@ export function TableV3(props: ITableV3Props): JSX.Element {
{/* When resizing any column we will render this special memoized version of our table body */}
{table.getState().columnSizingInfo.isResizingColumn ? (
-
+
) : (
-
+
)}
);
}
+
+TableV3.defaultProps = {
+ customClassName: '',
+ virtualiserRef: null,
+};
diff --git a/frontend/src/components/TimelineV2/TimelineV2.tsx b/frontend/src/components/TimelineV2/TimelineV2.tsx
index f7c0488bdfb..a52dafc5bc4 100644
--- a/frontend/src/components/TimelineV2/TimelineV2.tsx
+++ b/frontend/src/components/TimelineV2/TimelineV2.tsx
@@ -42,6 +42,8 @@ function TimelineV2(props: ITimelineV2Props): JSX.Element {
return
{intervals &&
@@ -70,14 +72,14 @@ function TimelineV2(props: ITimelineV2Props): JSX.Element {
{interval.label}
diff --git a/frontend/src/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts
index 30da4e13625..65fdbffd684 100644
--- a/frontend/src/constants/reactQueryKeys.ts
+++ b/frontend/src/constants/reactQueryKeys.ts
@@ -21,6 +21,8 @@ export const REACT_QUERY_KEY = {
GET_HOST_LIST: 'GET_HOST_LIST',
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3',
+ GET_TRACE_V2_WATERFALL: 'GET_TRACE_V2_WATERFALL',
+ GET_TRACE_V2_FLAMEGRAPH: 'GET_TRACE_V2_FLAMEGRAPH',
GET_POD_LIST: 'GET_POD_LIST',
GET_NODE_LIST: 'GET_NODE_LIST',
GET_DEPLOYMENT_LIST: 'GET_DEPLOYMENT_LIST',
diff --git a/frontend/src/constants/theme.ts b/frontend/src/constants/theme.ts
index edc0eea75f5..8481f0578ba 100644
--- a/frontend/src/constants/theme.ts
+++ b/frontend/src/constants/theme.ts
@@ -1,4 +1,38 @@
const themeColors = {
+ traceDetailColors: {
+ robin: '#3F5ECC',
+ dodgerBlue: '#2F80ED',
+ mediumOrchid: '#BB6BD9',
+ seaBuckthorn: '#F2994A',
+ turquoiseBlue: '#56CCF2',
+ festivalOrange: '#F2C94C',
+ silver: '#BDBDBD',
+ outrageousOrange: '#FF6633',
+ roseBud: '#FFB399',
+ canary: '#FFFF99',
+ deepSkyBlue: '#00B3E6',
+ goldTips: '#E6B333',
+ royalBlue: '#3366E6',
+ avocado: '#999966',
+ mintGreen: '#99FF99',
+ lima: '#80B300',
+ olive: '#809900',
+ beautyBush: '#E6B3B3',
+ danube: '#6680B3',
+ oliveDrab: '#66991A',
+ lavenderRose: '#FF99E6',
+ electricLime: '#CCFF1A',
+ turquoise: '#33FFCC',
+ gladeGreen: '#66994D',
+ hemlock: '#66664D',
+ vidaLoca: '#4D8000',
+ mediumAquamarine: '#66CDAA',
+ lavender: '#E6E6FA',
+ thistle: '#D8BFD8',
+ yellow: '#FFFF00',
+ purple: '#800080',
+ cyan: '#00FFFF',
+ },
chartcolors: {
robin: '#3F5ECC',
dodgerBlue: '#2F80ED',
diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx
index 24d9c2b9684..9a47baa8114 100644
--- a/frontend/src/container/AppLayout/index.tsx
+++ b/frontend/src/container/AppLayout/index.tsx
@@ -430,7 +430,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
? 0
: '0 1rem',
- ...(isTraceDetailsView() ? { marginRight: 0 } : {}),
+ ...(isTraceDetailsView() ? { margin: 0 } : {}),
}}
>
{isToDisplayLayout && !renderFullScreen &&
}
diff --git a/frontend/src/container/LogDetailedView/utils.tsx b/frontend/src/container/LogDetailedView/utils.tsx
index da62f97f8e2..05d65259f60 100644
--- a/frontend/src/container/LogDetailedView/utils.tsx
+++ b/frontend/src/container/LogDetailedView/utils.tsx
@@ -186,7 +186,7 @@ export const aggregateAttributesResourcesToString = (logData: ILog): string => {
id: logData.id,
severityNumber: logData.severityNumber,
severityText: logData.severityText,
- spanId: logData.spanId,
+ spanID: logData.spanID,
timestamp: logData.timestamp,
traceFlags: logData.traceFlags,
traceId: logData.traceId,
diff --git a/frontend/src/container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph.styles.scss b/frontend/src/container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph.styles.scss
new file mode 100644
index 00000000000..504acf4f02b
--- /dev/null
+++ b/frontend/src/container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph.styles.scss
@@ -0,0 +1,145 @@
+.flamegraph {
+ display: flex;
+ height: 30vh;
+ border-bottom: 1px solid var(--bg-slate-400);
+
+ .flamegraph-chart {
+ padding: 15px;
+
+ .loading-skeleton {
+ justify-content: center;
+ align-items: center;
+ }
+ }
+
+ .flamegraph-stats {
+ display: flex;
+ flex-direction: column;
+ border-right: 1px solid var(--bg-slate-400);
+ overflow-y: auto;
+ overflow-x: hidden;
+ padding: 16px 20px;
+
+ .exec-time-service {
+ display: flex;
+ height: 30px;
+ flex-shrink: 0;
+ justify-content: center;
+ align-items: center;
+ border-radius: 2px 0px 0px 2px;
+ border: 1px solid var(--bg-slate-400);
+ background: var(--bg-slate-400);
+ box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
+ margin-bottom: 16px;
+ color: var(--bg-vanilla-100);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 18px; /* 150% */
+ letter-spacing: -0.06px;
+ }
+
+ .stats {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ overflow-y: auto;
+ overflow-x: hidden;
+
+ &::-webkit-scrollbar {
+ width: 0rem;
+ }
+
+ .value-row {
+ display: flex;
+ justify-content: space-between;
+
+ .service-name {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 80%;
+
+ .service-text {
+ color: var(--bg-vanilla-400);
+ font-family: 'Inter';
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+ width: 80%;
+ }
+
+ .square-box {
+ height: 8px;
+ width: 8px;
+ }
+ }
+
+ .progress-service {
+ display: flex;
+ align-items: center;
+ width: 100px;
+ gap: 8px;
+ justify-content: flex-start;
+ flex-shrink: 0;
+
+ .service-progress-indicator {
+ width: fit-content;
+ margin-inline-end: 0px !important;
+ margin-bottom: 0px !important;
+
+ .ant-progress-inner {
+ width: 30px;
+ }
+ }
+
+ .percent-value {
+ color: var(--bg-vanilla-100);
+ text-align: right;
+ font-family: 'Inter';
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+ letter-spacing: 0.48px;
+ font-variant-numeric: lining-nums tabular-nums slashed-zero;
+ }
+ }
+ }
+ }
+ }
+}
+
+.lightMode {
+ .flamegraph {
+ border-bottom: 1px solid var(--bg-vanilla-300);
+
+ .flamegraph-stats {
+ border-right: 1px solid var(--bg-vanilla-300);
+
+ .exec-time-service {
+ border: 1px solid var(--bg-vanilla-400);
+ background: var(--bg-vanilla-400);
+ color: var(--bg-ink-100);
+ }
+
+ .stats {
+ .value-row {
+ .service-name {
+ .service-text {
+ color: var(--bg-ink-400);
+ }
+ }
+
+ .progress-service {
+ .percent-value {
+ color: var(--bg-ink-100);
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/src/container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph.tsx b/frontend/src/container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph.tsx
new file mode 100644
index 00000000000..c01fc274d48
--- /dev/null
+++ b/frontend/src/container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph.tsx
@@ -0,0 +1,177 @@
+import './PaginatedTraceFlamegraph.styles.scss';
+
+import { Progress, Skeleton, Tooltip, Typography } from 'antd';
+import { AxiosError } from 'axios';
+import Spinner from 'components/Spinner';
+import { themeColors } from 'constants/theme';
+import useGetTraceFlamegraph from 'hooks/trace/useGetTraceFlamegraph';
+import { useIsDarkMode } from 'hooks/useDarkMode';
+import useUrlQuery from 'hooks/useUrlQuery';
+import { generateColor } from 'lib/uPlotLib/utils/generateColor';
+import { useEffect, useMemo, useState } from 'react';
+import { useParams } from 'react-router-dom';
+import { TraceDetailFlamegraphURLProps } from 'types/api/trace/getTraceFlamegraph';
+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/Success';
+
+interface ITraceFlamegraphProps {
+ serviceExecTime: Record
;
+ startTime: number;
+ endTime: number;
+ traceFlamegraphStatsWidth: number;
+ selectedSpan: Span | undefined;
+}
+
+function TraceFlamegraph(props: ITraceFlamegraphProps): JSX.Element {
+ const {
+ serviceExecTime,
+ startTime,
+ endTime,
+ traceFlamegraphStatsWidth,
+ selectedSpan,
+ } = props;
+ const { id: traceId } = useParams();
+ const urlQuery = useUrlQuery();
+ const [firstSpanAtFetchLevel, setFirstSpanAtFetchLevel] = useState(
+ urlQuery.get('spanId') || '',
+ );
+
+ useEffect(() => {
+ setFirstSpanAtFetchLevel(urlQuery.get('spanId') || '');
+ }, [urlQuery]);
+
+ const { data, isFetching, error } = useGetTraceFlamegraph({
+ traceId,
+ selectedSpanId: firstSpanAtFetchLevel,
+ });
+ const isDarkMode = useIsDarkMode();
+
+ // get the current state of trace flamegraph based on the API lifecycle
+ const traceFlamegraphState = useMemo(() => {
+ if (isFetching) {
+ if (
+ data &&
+ data.payload &&
+ data.payload.spans &&
+ data.payload.spans.length > 0
+ ) {
+ return TraceFlamegraphStates.FETCHING_WITH_OLD_DATA_PRESENT;
+ }
+ return TraceFlamegraphStates.LOADING;
+ }
+ if (error) {
+ return TraceFlamegraphStates.ERROR;
+ }
+ if (
+ data &&
+ data.payload &&
+ data.payload.spans &&
+ data.payload.spans.length === 0
+ ) {
+ return TraceFlamegraphStates.NO_DATA;
+ }
+
+ return TraceFlamegraphStates.SUCCESS;
+ }, [error, isFetching, data]);
+
+ // capture the spans from the response, since we do not need to do any manipulation on the same we will keep this as a simple constant [ memoized ]
+ const spans = useMemo(() => data?.payload?.spans || [], [
+ data?.payload?.spans,
+ ]);
+
+ // get the content based on the current state of the trace waterfall
+ const getContent = useMemo(() => {
+ switch (traceFlamegraphState) {
+ case TraceFlamegraphStates.LOADING:
+ return (
+
+
+
+ );
+ case TraceFlamegraphStates.ERROR:
+ return ;
+ case TraceFlamegraphStates.NO_DATA:
+ return ;
+ case TraceFlamegraphStates.SUCCESS:
+ case TraceFlamegraphStates.FETCHING_WITH_OLD_DATA_PRESENT:
+ return (
+
+ );
+ default:
+ return ;
+ }
+ }, [
+ data?.payload?.endTimestampMillis,
+ data?.payload?.startTimestampMillis,
+ error,
+ firstSpanAtFetchLevel,
+ selectedSpan,
+ spans,
+ traceFlamegraphState,
+ traceId,
+ ]);
+
+ return (
+
+
+
% exec time
+
+ {Object.keys(serviceExecTime).map((service) => {
+ const spread = endTime - startTime;
+ const value = (serviceExecTime[service] * 100) / spread;
+ const color = generateColor(
+ service,
+ isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
+ );
+ return (
+
+
+
+
+
+ {parseFloat(value.toFixed(2))}%
+
+
+
+ );
+ })}
+
+
+
+ {getContent}
+
+
+ );
+}
+
+export default TraceFlamegraph;
diff --git a/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Error/Error.styles.scss b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Error/Error.styles.scss
new file mode 100644
index 00000000000..1b837b5ad97
--- /dev/null
+++ b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Error/Error.styles.scss
@@ -0,0 +1,31 @@
+.error-flamegraph {
+ display: flex;
+ gap: 4px;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ height: 15vh;
+
+ .error-flamegraph-img {
+ height: 32px;
+ width: 32px;
+ }
+
+ .no-data-text {
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 18px; /* 128.571% */
+ letter-spacing: -0.07px;
+ }
+}
+
+.lightMode {
+ .error-flamegraph {
+ .no-data-text {
+ color: var(--bg-ink-400);
+ }
+ }
+}
diff --git a/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Error/Error.tsx b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Error/Error.tsx
new file mode 100644
index 00000000000..a9a4d63f94f
--- /dev/null
+++ b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Error/Error.tsx
@@ -0,0 +1,29 @@
+import './Error.styles.scss';
+
+import { Tooltip, Typography } from 'antd';
+import { AxiosError } from 'axios';
+
+interface IErrorProps {
+ error: AxiosError;
+}
+
+function Error(props: IErrorProps): JSX.Element {
+ const { error } = props;
+
+ return (
+
+
+
+
+ {error?.message || 'Something went wrong!'}
+
+
+
+ );
+}
+
+export default Error;
diff --git a/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/NoData/NoData.tsx b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/NoData/NoData.tsx
new file mode 100644
index 00000000000..0be04ffc234
--- /dev/null
+++ b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/NoData/NoData.tsx
@@ -0,0 +1,12 @@
+import { Typography } from 'antd';
+
+interface INoDataProps {
+ id: string;
+}
+
+function NoData(props: INoDataProps): JSX.Element {
+ const { id } = props;
+ return No Trace found with the id: {id} ;
+}
+
+export default NoData;
diff --git a/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.styles.scss b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.styles.scss
new file mode 100644
index 00000000000..16501c6b01c
--- /dev/null
+++ b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.styles.scss
@@ -0,0 +1,28 @@
+.trace-flamegraph {
+ height: 90%;
+ overflow-x: hidden;
+ overflow-y: auto;
+
+ .trace-flamegraph-virtuoso {
+ overflow-x: hidden;
+
+ .flamegraph-row {
+ display: flex;
+ align-items: center;
+ height: 18px;
+ padding-bottom: 6px;
+
+ .span-item {
+ position: absolute;
+ height: 12px;
+ background-color: yellow;
+ border-radius: 6px;
+ cursor: pointer;
+ }
+ }
+
+ &::-webkit-scrollbar {
+ width: 0rem;
+ }
+ }
+}
diff --git a/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.tsx b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.tsx
new file mode 100644
index 00000000000..bd99cebee64
--- /dev/null
+++ b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.tsx
@@ -0,0 +1,154 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+import './Success.styles.scss';
+
+import { Tooltip } 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 {
+ Dispatch,
+ SetStateAction,
+ useCallback,
+ useEffect,
+ useRef,
+ useState,
+} from 'react';
+import { useHistory, useLocation } from 'react-router-dom';
+import { ListRange, Virtuoso, VirtuosoHandle } from 'react-virtuoso';
+import { FlamegraphSpan } from 'types/api/trace/getTraceFlamegraph';
+import { Span } from 'types/api/trace/getTraceV2';
+
+interface ITraceMetadata {
+ startTime: number;
+ endTime: number;
+}
+
+interface ISuccessProps {
+ spans: FlamegraphSpan[][];
+ firstSpanAtFetchLevel: string;
+ setFirstSpanAtFetchLevel: Dispatch>;
+ traceMetadata: ITraceMetadata;
+ selectedSpan: Span | undefined;
+}
+
+function Success(props: ISuccessProps): JSX.Element {
+ const {
+ spans,
+ setFirstSpanAtFetchLevel,
+ traceMetadata,
+ firstSpanAtFetchLevel,
+ selectedSpan,
+ } = props;
+ const { search } = useLocation();
+ const history = useHistory();
+ const isDarkMode = useIsDarkMode();
+ const virtuosoRef = useRef(null);
+ const [hoveredSpanId, setHoveredSpanId] = useState('');
+ const renderSpanLevel = useCallback(
+ (_: number, spans: FlamegraphSpan[]): JSX.Element => (
+
+ {spans.map((span) => {
+ const spread = traceMetadata.endTime - traceMetadata.startTime;
+ const leftOffset =
+ ((span.timestamp - traceMetadata.startTime) * 100) / spread;
+ let width = ((span.durationNano / 1e6) * 100) / spread;
+ if (width > 100) {
+ width = 100;
+ }
+ const toolTipText = `${span.name}`;
+ const searchParams = new URLSearchParams(search);
+
+ let color = generateColor(span.serviceName, themeColors.traceDetailColors);
+
+ const selectedSpanColor = isDarkMode
+ ? Color(color).lighten(0.7)
+ : Color(color).darken(0.7);
+
+ if (span.hasError) {
+ color = `var(--bg-cherry-500)`;
+ }
+
+ return (
+
+ setHoveredSpanId(span.spanId)}
+ onMouseLeave={(): void => setHoveredSpanId('')}
+ onClick={(event): void => {
+ event.stopPropagation();
+ event.preventDefault();
+ searchParams.set('spanId', span.spanId);
+ history.replace({ search: searchParams.toString() });
+ }}
+ />
+
+ );
+ })}
+
+ ),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [traceMetadata.endTime, traceMetadata.startTime, selectedSpan, hoveredSpanId],
+ );
+
+ const handleRangeChanged = useCallback(
+ (range: ListRange) => {
+ // if there are less than 50 levels on any load that means a single API call is sufficient
+ if (spans.length < 50) {
+ return;
+ }
+
+ const { startIndex, endIndex } = range;
+ if (startIndex === 0 && spans[0][0].level !== 0) {
+ setFirstSpanAtFetchLevel(spans[0][0].spanId);
+ }
+
+ if (endIndex === spans.length - 1) {
+ setFirstSpanAtFetchLevel(spans[spans.length - 1][0].spanId);
+ }
+ },
+ [setFirstSpanAtFetchLevel, spans],
+ );
+
+ useEffect(() => {
+ const index = spans.findIndex(
+ (span) => span[0].spanId === firstSpanAtFetchLevel,
+ );
+
+ virtuosoRef.current?.scrollToIndex({
+ index,
+ behavior: 'auto',
+ });
+ }, [firstSpanAtFetchLevel, spans]);
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export default Success;
diff --git a/frontend/src/container/PaginatedTraceFlamegraph/constants.ts b/frontend/src/container/PaginatedTraceFlamegraph/constants.ts
new file mode 100644
index 00000000000..2f7cb4958bf
--- /dev/null
+++ b/frontend/src/container/PaginatedTraceFlamegraph/constants.ts
@@ -0,0 +1,7 @@
+export enum TraceFlamegraphStates {
+ LOADING = 'LOADING',
+ SUCCESS = 'SUCCESS',
+ NO_DATA = 'NO_DATA',
+ ERROR = 'ERROR',
+ FETCHING_WITH_OLD_DATA_PRESENT = 'FETCHING_WTIH_OLD_DATA_PRESENT',
+}
diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchDropdown.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchDropdown.tsx
index 2e9f8341e33..577b72ff839 100644
--- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchDropdown.tsx
+++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchDropdown.tsx
@@ -65,7 +65,7 @@ export default function QueryBuilderSearchDropdown(
)}
{menu}
- {!searchValue && tags.length === 0 && (
+ {!searchValue && tags.length === 0 && exampleQueries.length > 0 && (
Example Queries
diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss
index 60eec0bdb65..c7a96456e92 100644
--- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss
+++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss
@@ -9,7 +9,7 @@
.show-all-filters {
.content {
.rc-virtual-list-holder {
- height: 100px;
+ height: 115px;
}
}
}
diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx
index e319ae3d9ee..48ee2d0a604 100644
--- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx
+++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx
@@ -224,7 +224,7 @@ function QueryBuilderSearchV2(
const { data, isFetching } = useGetAggregateKeys(
{
- searchText: searchValue,
+ searchText: searchValue?.split(' ')[0],
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
aggregateAttribute: query.aggregateAttribute.key,
diff --git a/frontend/src/container/SpanDetailsDrawer/Attributes/Attributes.styles.scss b/frontend/src/container/SpanDetailsDrawer/Attributes/Attributes.styles.scss
new file mode 100644
index 00000000000..5c686e5aec0
--- /dev/null
+++ b/frontend/src/container/SpanDetailsDrawer/Attributes/Attributes.styles.scss
@@ -0,0 +1,89 @@
+.attributes-corner {
+ display: flex;
+ flex-direction: column;
+
+ .no-data {
+ height: 400px;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .search-input {
+ margin: 12px;
+ width: auto;
+ }
+
+ .attributes-container {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 12px;
+
+ .item {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ justify-content: flex-start;
+
+ .item-key {
+ color: var(--bg-vanilla-100);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ }
+
+ .value-wrapper {
+ display: flex;
+ padding: 2px 8px;
+ align-items: center;
+ width: fit-content;
+ max-width: 100%;
+ gap: 8px;
+ border-radius: 50px;
+ border: 1px solid var(--bg-slate-400);
+ background: var(--bg-slate-500);
+ .item-value {
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: 0.56px;
+ }
+ }
+ }
+ }
+
+ .border-top {
+ border-top: 1px solid var(--bg-slate-400);
+ }
+}
+
+.lightMode {
+ .attributes-corner {
+ .attributes-container {
+ .item {
+ .item-key {
+ color: var(--bg-ink-100);
+ }
+
+ .value-wrapper {
+ border: 1px solid var(--bg-vanilla-300);
+ background: var(--bg-vanilla-300);
+
+ .item-value {
+ color: var(--bg-ink-400);
+ }
+ }
+ }
+ }
+
+ .border-top {
+ border-top: 1px solid var(--bg-vanilla-300);
+ }
+ }
+}
diff --git a/frontend/src/container/SpanDetailsDrawer/Attributes/Attributes.tsx b/frontend/src/container/SpanDetailsDrawer/Attributes/Attributes.tsx
new file mode 100644
index 00000000000..4199aa37ec7
--- /dev/null
+++ b/frontend/src/container/SpanDetailsDrawer/Attributes/Attributes.tsx
@@ -0,0 +1,65 @@
+import './Attributes.styles.scss';
+
+import { Input, Tooltip, Typography } from 'antd';
+import cx from 'classnames';
+import { flattenObject } from 'container/LogDetailedView/utils';
+import { useMemo, useState } from 'react';
+import { Span } from 'types/api/trace/getTraceV2';
+
+import NoData from '../NoData/NoData';
+
+interface IAttributesProps {
+ span: Span;
+ isSearchVisible: boolean;
+}
+
+function Attributes(props: IAttributesProps): JSX.Element {
+ const { span, isSearchVisible } = props;
+ const [fieldSearchInput, setFieldSearchInput] = useState
('');
+
+ const flattenSpanData: Record = useMemo(
+ () => (span.tagMap ? flattenObject(span.tagMap) : {}),
+ [span],
+ );
+
+ const datasource = Object.keys(flattenSpanData)
+ .filter((attribute) =>
+ attribute.toLowerCase().includes(fieldSearchInput.toLowerCase()),
+ )
+ .map((key) => ({ field: key, value: flattenSpanData[key] }));
+
+ return (
+
+ {datasource.length === 0 &&
}
+ {isSearchVisible && datasource.length > 0 && (
+
setFieldSearchInput(e.target.value)}
+ />
+ )}
+
+ {datasource.map((item) => (
+
+
+ {item.field}
+
+
+
+
+ {item.value}
+
+
+
+
+ ))}
+
+
+ );
+}
+
+export default Attributes;
diff --git a/frontend/src/container/SpanDetailsDrawer/Events/Events.styles.scss b/frontend/src/container/SpanDetailsDrawer/Events/Events.styles.scss
new file mode 100644
index 00000000000..70382bf7152
--- /dev/null
+++ b/frontend/src/container/SpanDetailsDrawer/Events/Events.styles.scss
@@ -0,0 +1,188 @@
+.events-table {
+ .no-events {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 400px;
+ }
+ .events-container {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 12px;
+
+ .event {
+ .ant-collapse {
+ border: none;
+ }
+ .ant-collapse-content {
+ border-top: none;
+ }
+
+ .ant-collapse-item {
+ border-bottom: 0px;
+ }
+
+ .ant-collapse-content-box {
+ border: 1px solid var(--bg-slate-500);
+ border-top: none;
+ }
+ .ant-collapse-header {
+ display: flex;
+ padding: 8px 6px;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ border-radius: 2px;
+ border: 1px solid var(--bg-slate-500);
+ background: var(--bg-ink-400);
+
+ color: var(--bg-vanilla-100);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 18px; /* 128.571% */
+ letter-spacing: -0.07px;
+
+ .ant-collapse-expand-icon {
+ padding-inline-start: 0px;
+ padding-inline-end: 0px;
+ }
+
+ .collapse-title {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+
+ .diamond {
+ fill: var(--bg-cherry-500);
+ }
+ }
+ }
+ .event-details {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ .attribute-container {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ .attribute-key {
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ }
+
+ .timestamp-container {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+
+ .timestamp-text {
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ }
+
+ .attribute-value {
+ display: flex;
+ padding: 2px 8px;
+ width: fit-content;
+ align-items: center;
+ gap: 8px;
+ border-radius: 50px;
+ border: 1px solid var(--bg-slate-400);
+ background: var(--bg-slate-500);
+
+ color: var(--bg-vanilla-100);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ }
+ }
+
+ .wrapper {
+ display: flex;
+ padding: 2px 8px;
+ width: fit-content;
+ max-width: 100%;
+ align-items: center;
+ gap: 8px;
+ border-radius: 50px;
+ border: 1px solid var(--bg-slate-400);
+ background: var(--bg-slate-500);
+ .attribute-value {
+ color: var(--bg-vanilla-100);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+.lightMode {
+ .events-table {
+ .events-container {
+ .event {
+ .ant-collapse-content-box {
+ border: 1px solid var(--bg-vanilla-300);
+ }
+ .ant-collapse-header {
+ border: 1px solid var(--bg-vanilla-300);
+ background: var(--bg-vanilla-300);
+ color: var(--bg-ink-100);
+ border-bottom: none;
+ }
+ .event-details {
+ .attribute-container {
+ .attribute-key {
+ color: var(--bg-ink-400);
+ }
+
+ .timestamp-container {
+ .timestamp-text {
+ color: var(--bg-ink-400);
+ }
+
+ .attribute-value {
+ border: 1px solid var(--bg-vanilla-300);
+ background: var(--bg-vanilla-300);
+
+ color: var(--bg-ink-100);
+ }
+ }
+
+ .wrapper {
+ border: 1px solid var(--bg-vanilla-300);
+ background: var(--bg-vanilla-300);
+ .attribute-value {
+ color: var(--bg-ink-100);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/src/container/SpanDetailsDrawer/Events/Events.tsx b/frontend/src/container/SpanDetailsDrawer/Events/Events.tsx
new file mode 100644
index 00000000000..e339f3f9a25
--- /dev/null
+++ b/frontend/src/container/SpanDetailsDrawer/Events/Events.tsx
@@ -0,0 +1,118 @@
+import './Events.styles.scss';
+
+import { Collapse, Input, Tooltip, Typography } from 'antd';
+import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
+import { Diamond } from 'lucide-react';
+import { useMemo, useState } from 'react';
+import { Event, Span } from 'types/api/trace/getTraceV2';
+
+import NoData from '../NoData/NoData';
+
+interface IEventsTableProps {
+ span: Span;
+ startTime: number;
+ isSearchVisible: boolean;
+}
+
+function EventsTable(props: IEventsTableProps): JSX.Element {
+ const { span, startTime, isSearchVisible } = props;
+ const [fieldSearchInput, setFieldSearchInput] = useState('');
+ const events: Event[] = useMemo(() => {
+ const tempEvents = [];
+ for (let i = 0; i < span.event?.length; i++) {
+ const parsedEvent = JSON.parse(span.event[i]);
+ tempEvents.push(parsedEvent);
+ }
+ return tempEvents;
+ }, [span.event]);
+
+ return (
+
+ {events.length === 0 && (
+
+
+
+ )}
+
+ {isSearchVisible && (
+
setFieldSearchInput(e.target.value)}
+ />
+ )}
+ {events
+ .filter((eve) =>
+ eve.name.toLowerCase().includes(fieldSearchInput.toLowerCase()),
+ )
+ .map((event) => (
+
+
+
+
+ {event.name}
+
+
+ ),
+ children: (
+
+
+
+ Start Time
+
+
+
+ {getYAxisFormattedValue(
+ `${event.timeUnixNano / 1e6 - startTime}`,
+ 'ms',
+ )}
+
+
+ after the start
+
+
+
+ {event.attributeMap &&
+ Object.keys(event.attributeMap).map((attributeKey) => (
+
+
+
+ {attributeKey}
+
+
+
+
+
+
+ {event.attributeMap[attributeKey]}
+
+
+
+
+ ))}
+
+ ),
+ },
+ ]}
+ />
+
+ ))}
+
+
+ );
+}
+
+export default EventsTable;
diff --git a/frontend/src/container/SpanDetailsDrawer/NoData/NoData.styles.scss b/frontend/src/container/SpanDetailsDrawer/NoData/NoData.styles.scss
new file mode 100644
index 00000000000..e40fb340a9e
--- /dev/null
+++ b/frontend/src/container/SpanDetailsDrawer/NoData/NoData.styles.scss
@@ -0,0 +1,28 @@
+.no-data {
+ display: flex;
+ gap: 4px;
+ flex-direction: column;
+
+ .no-data-img {
+ height: 32px;
+ width: 32px;
+ }
+
+ .no-data-text {
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 18px; /* 128.571% */
+ letter-spacing: -0.07px;
+ }
+}
+
+.lightMode {
+ .no-data {
+ .no-data-text {
+ color: var(--bg-ink-400);
+ }
+ }
+}
diff --git a/frontend/src/container/SpanDetailsDrawer/NoData/NoData.tsx b/frontend/src/container/SpanDetailsDrawer/NoData/NoData.tsx
new file mode 100644
index 00000000000..df4e5f38c3b
--- /dev/null
+++ b/frontend/src/container/SpanDetailsDrawer/NoData/NoData.tsx
@@ -0,0 +1,22 @@
+import './NoData.styles.scss';
+
+import { Typography } from 'antd';
+
+interface INoDataProps {
+ name: string;
+}
+
+function NoData(props: INoDataProps): JSX.Element {
+ const { name } = props;
+
+ return (
+
+
+
+ No {name} found for selected span
+
+
+ );
+}
+
+export default NoData;
diff --git a/frontend/src/container/SpanDetailsDrawer/SpanDetailsDrawer.styles.scss b/frontend/src/container/SpanDetailsDrawer/SpanDetailsDrawer.styles.scss
new file mode 100644
index 00000000000..572f57396ae
--- /dev/null
+++ b/frontend/src/container/SpanDetailsDrawer/SpanDetailsDrawer.styles.scss
@@ -0,0 +1,263 @@
+.span-details-drawer {
+ display: flex;
+ flex-direction: column;
+ width: 330px;
+ border-left: 1px solid var(--bg-slate-400);
+ overflow-y: auto;
+
+ &::-webkit-scrollbar {
+ width: 0.1rem;
+ }
+
+ .header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 48px;
+ padding: 12px;
+ border-bottom: 1px solid var(--bg-slate-400);
+
+ .heading {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ .dot {
+ height: 8px;
+ width: 8px;
+ border-radius: 2px;
+ background: var(--bg-cherry-500);
+ }
+
+ .text {
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ }
+ }
+ }
+
+ .description {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 10px 12px;
+
+ .item {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ .attribute-key {
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 11px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 18px; /* 163.636% */
+ letter-spacing: 0.44px;
+ text-transform: uppercase;
+ }
+
+ .value-wrapper {
+ display: flex;
+ padding: 2px 8px;
+ align-items: center;
+ width: fit-content;
+ max-width: 100%;
+ border-radius: 50px;
+ border: 1px solid var(--bg-slate-400);
+ background: var(--bg-slate-500);
+
+ .attribute-value {
+ color: var(--bg-vanilla-400);
+ font-family: 'Inter';
+ font-size: 14px;
+ font-style: normal;
+ width: 100%;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: 0.28px;
+ }
+ }
+
+ .service {
+ display: flex;
+ padding: 2px 8px;
+ align-items: center;
+ gap: 8px;
+ border-radius: 50px;
+ border: 1px solid var(--bg-slate-400);
+ background: var(--bg-slate-500);
+ width: fit-content;
+
+ .dot {
+ height: 4px;
+ width: 4px;
+ }
+
+ .value-wrapper {
+ display: flex;
+ padding: 0px;
+ align-items: center;
+ width: fit-content;
+ max-width: 100%;
+ border-radius: 0px;
+ border: none;
+ background: var(--bg-slate-500);
+
+ .service-value {
+ color: var(--bg-vanilla-400);
+ font-family: 'Inter';
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: 0.28px;
+ }
+ }
+ }
+ }
+ }
+
+ .related-logs {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: fit-content;
+ padding: 5px 12px;
+ margin: 10px 12px;
+ box-shadow: none;
+
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ }
+
+ .attributes-events {
+ .details-drawer-tabs {
+ .ant-tabs-extra-content {
+ display: flex;
+ align-items: center;
+
+ .search-icon {
+ width: 33px;
+ padding-right: 12px;
+ }
+ }
+
+ .ant-tabs-nav::before {
+ border-bottom: 1px solid var(--bg-slate-400) !important;
+ }
+
+ .attributes-tab-btn {
+ display: flex;
+ align-items: center;
+ }
+ .attributes-tab-btn:hover {
+ background: unset;
+ }
+
+ .events-tab-btn {
+ display: flex;
+ align-items: center;
+ }
+
+ .events-tab-btn:hover {
+ background: unset;
+ }
+ }
+ }
+}
+
+.span-details-drawer-docked {
+ width: 48px;
+
+ .header {
+ justify-content: center;
+ }
+}
+
+.lightMode {
+ .span-details-drawer {
+ border-left: 1px solid var(--bg-vanilla-300);
+
+ .header {
+ border-bottom: 1px solid var(--bg-vanilla-300);
+
+ .heading {
+ .text {
+ color: var(--bg-ink-400);
+ }
+ }
+ }
+
+ .description {
+ .item {
+ .attribute-key {
+ color: var(--bg-ink-400);
+ }
+
+ .value-wrapper {
+ border: 1px solid var(--bg-vanilla-300);
+ background: var(--bg-vanilla-300);
+
+ .attribute-value {
+ color: var(--bg-ink-400);
+ }
+ }
+
+ .service {
+ border: 1px solid var(--bg-vanilla-300);
+ background: var(--bg-vanilla-300);
+
+ .value-wrapper {
+ background: var(--bg-vanilla-300);
+ border: none;
+
+ .service-value {
+ color: var(--bg-ink-400);
+ }
+ }
+ }
+ }
+ }
+
+ .related-logs {
+ color: var(--bg-ink-400);
+ }
+
+ .attributes-events {
+ .details-drawer-tabs {
+ .ant-tabs-nav::before {
+ border-bottom: 1px solid var(--bg-vanilla-300) !important;
+ }
+
+ .ant-tabs-nav-wrap {
+ height: 32px;
+ }
+
+ .ant-tabs-tab {
+ border: none;
+ background-color: var(--bg-vanilla-200);
+
+ .ant-btn {
+ border-bottom: 1px solid var(--bg-vanilla-300);
+ }
+ }
+
+ .ant-tabs-ink-bar {
+ background: #4e74f8 !important;
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/src/container/SpanDetailsDrawer/SpanDetailsDrawer.tsx b/frontend/src/container/SpanDetailsDrawer/SpanDetailsDrawer.tsx
new file mode 100644
index 00000000000..222202ba446
--- /dev/null
+++ b/frontend/src/container/SpanDetailsDrawer/SpanDetailsDrawer.tsx
@@ -0,0 +1,208 @@
+import './SpanDetailsDrawer.styles.scss';
+
+import { Button, Tabs, TabsProps, Tooltip, Typography } from 'antd';
+import cx from 'classnames';
+import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
+import { QueryParams } from 'constants/query';
+import ROUTES from 'constants/routes';
+import { themeColors } from 'constants/theme';
+import { getTraceToLogsQuery } from 'container/TraceDetail/SelectedSpanDetails/config';
+import createQueryParams from 'lib/createQueryParams';
+import history from 'lib/history';
+import { generateColor } from 'lib/uPlotLib/utils/generateColor';
+import { Anvil, Bookmark, PanelRight, Search } from 'lucide-react';
+import { Dispatch, SetStateAction, useState } from 'react';
+import { Span } from 'types/api/trace/getTraceV2';
+import { formatEpochTimestamp } from 'utils/timeUtils';
+
+import Attributes from './Attributes/Attributes';
+import Events from './Events/Events';
+
+interface ISpanDetailsDrawerProps {
+ isSpanDetailsDocked: boolean;
+ setIsSpanDetailsDocked: Dispatch
>;
+ selectedSpan: Span | undefined;
+ traceID: string;
+ traceStartTime: number;
+ traceEndTime: number;
+}
+
+function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
+ const {
+ isSpanDetailsDocked,
+ setIsSpanDetailsDocked,
+ selectedSpan,
+ traceStartTime,
+ traceID,
+ traceEndTime,
+ } = props;
+
+ const [isSearchVisible, setIsSearchVisible] = useState(false);
+ const color = generateColor(
+ selectedSpan?.serviceName || '',
+ themeColors.traceDetailColors,
+ );
+
+ function getItems(span: Span, startTime: number): TabsProps['items'] {
+ return [
+ {
+ label: (
+ }
+ className="attributes-tab-btn"
+ >
+ Attributes
+
+ ),
+ key: 'attributes',
+ children: ,
+ },
+ {
+ label: (
+ } className="events-tab-btn">
+ Events
+
+ ),
+ key: 'events',
+ children: (
+
+ ),
+ },
+ ];
+ }
+ const onLogsHandler = (): void => {
+ const query = getTraceToLogsQuery(traceID, traceStartTime, traceEndTime);
+
+ history.push(
+ `${ROUTES.LOGS_EXPLORER}?${createQueryParams({
+ [QueryParams.compositeQuery]: JSON.stringify(query),
+ // we subtract 1000 milliseconds from the start time to handle the cases when the trace duration is in nanoseconds
+ [QueryParams.startTime]: traceStartTime - 1000,
+ // we add 1000 milliseconds to the end time for nano second duration traces
+ [QueryParams.endTime]: traceEndTime + 1000,
+ })}`,
+ );
+ };
+
+ return (
+
+
+ {!isSpanDetailsDocked && (
+
+ )}
+ setIsSpanDetailsDocked((prev) => !prev)}
+ />
+
+ {selectedSpan && !isSpanDetailsDocked && (
+ <>
+
+
+
span name
+
+
+
+ {selectedSpan.name}
+
+
+
+
+
+
span id
+
+
+ {selectedSpan.spanId}
+
+
+
+
+
start time
+
+
+ {formatEpochTimestamp(selectedSpan.timestamp)}
+
+
+
+
+
duration
+
+
+ {getYAxisFormattedValue(`${selectedSpan.durationNano}`, 'ns')}
+
+
+
+
+
service
+
+
+
+
+
+ {selectedSpan.serviceName}
+
+
+
+
+
+
+
span kind
+
+
+ {selectedSpan.spanKind}
+
+
+
+
+
+ status code string
+
+
+
+ {selectedSpan.statusCodeString}
+
+
+
+
+
+
+ Go to related logs
+
+
+
+ setIsSearchVisible((prev) => !prev)}
+ />
+ }
+ />
+
+ >
+ )}
+
+ );
+}
+
+export default SpanDetailsDrawer;
diff --git a/frontend/src/container/TraceMetadata/TraceMetadata.styles.scss b/frontend/src/container/TraceMetadata/TraceMetadata.styles.scss
new file mode 100644
index 00000000000..3abad00ca46
--- /dev/null
+++ b/frontend/src/container/TraceMetadata/TraceMetadata.styles.scss
@@ -0,0 +1,262 @@
+.trace-metadata {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0px 16px 0px 16px;
+
+ .metadata-info {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ .first-row {
+ display: flex;
+ align-items: center;
+
+ .previous-btn {
+ display: flex;
+ height: 30px;
+ padding: 6px 8px;
+ align-items: center;
+ gap: 4px;
+ border: 1px solid var(--bg-slate-300);
+ background: var(--bg-slate-500);
+ border-radius: 4px;
+ box-shadow: none;
+ }
+
+ .trace-name {
+ display: flex;
+ padding: 6px 8px;
+ margin-left: 6px;
+ align-items: center;
+ gap: 4px;
+ border: 1px solid var(--bg-slate-300);
+ border-radius: 4px 0px 0px 4px;
+ background: var(--bg-slate-500);
+
+ .drafting {
+ color: white;
+ }
+
+ .trace-id {
+ color: #fff;
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 18px; /* 128.571% */
+ letter-spacing: -0.07px;
+ }
+ }
+
+ .trace-id-value {
+ display: flex;
+ padding: 6px 8px;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ background: var(--bg-slate-400);
+ color: var(--Vanilla-400, #c0c1c3);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 18px; /* 128.571% */
+ letter-spacing: -0.07px;
+ border: 1px solid var(--bg-slate-300);
+ border-left: unset;
+ border-radius: 0px 4px 4px 0px;
+ }
+ }
+
+ .second-row {
+ display: flex;
+ gap: 24px;
+
+ .service-entry-info {
+ display: flex;
+ gap: 6px;
+ color: var(--bg-vanilla-400);
+ align-items: center;
+
+ .text {
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ }
+
+ .root-span-name {
+ display: flex;
+ padding: 2px 8px;
+ align-items: center;
+ gap: 8px;
+ border-radius: 50px;
+ border: 1px solid var(--bg-slate-400);
+ background: var(--bg-slate-500);
+ }
+ }
+
+ .trace-duration {
+ display: flex;
+ gap: 6px;
+ color: var(--bg-vanilla-400);
+ align-items: center;
+
+ .text {
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ }
+ }
+
+ .start-time-info {
+ display: flex;
+ gap: 6px;
+ color: var(--bg-vanilla-400);
+ align-items: center;
+
+ .text {
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ }
+ }
+ }
+ }
+
+ .datapoints-info {
+ display: flex;
+ gap: 16px;
+
+ .separator {
+ width: 1px;
+ background: #1d212d;
+ }
+
+ .data-point {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 4px;
+
+ .text {
+ color: var(--bg-vanilla-400);
+ text-align: center;
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 18px; /* 128.571% */
+ letter-spacing: -0.07px;
+ }
+
+ .value {
+ color: var(--bg-vanilla-100);
+ font-variant-numeric: lining-nums tabular-nums stacked-fractions
+ slashed-zero;
+ font-feature-settings: 'case' on, 'cpsp' on, 'dlig' on, 'salt' on;
+ font-family: Inter;
+ font-size: 20px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 28px; /* 140% */
+ letter-spacing: -0.1px;
+ text-transform: uppercase;
+ text-align: right;
+ }
+ }
+ }
+}
+
+.lightMode {
+ .trace-metadata {
+ .metadata-info {
+ .first-row {
+ .previous-btn {
+ border: 1px solid var(--bg-vanilla-300);
+ background: var(--bg-vanilla-200);
+ }
+
+ .trace-name {
+ border: 1px solid var(--bg-vanilla-300);
+ background: var(--bg-vanilla-200);
+ border-right: none;
+
+ .drafting {
+ color: var(--bg-ink-100);
+ }
+
+ .trace-id {
+ color: var(--bg-ink-100);
+ }
+ }
+
+ .trace-id-value {
+ background: var(--bg-vanilla-300);
+ color: var(--bg-ink-400);
+ border: 1px solid var(--bg-vanilla-300);
+ }
+ }
+
+ .second-row {
+ .service-entry-info {
+ color: var(--bg-ink-400);
+
+ .text {
+ color: var(--bg-ink-400);
+ }
+
+ .root-span-name {
+ border: 1px solid var(--bg-vanilla-300);
+ background: var(--bg-vanilla-300);
+ }
+ }
+
+ .trace-duration {
+ color: var(--bg-ink-400);
+
+ .text {
+ color: var(--bg-ink-400);
+ }
+ }
+
+ .start-time-info {
+ color: var(--bg-ink-400);
+
+ .text {
+ color: var(--bg-ink-400);
+ }
+ }
+ }
+ }
+
+ .datapoints-info {
+ .separator {
+ background: var(--bg-vanilla-300);
+ }
+
+ .data-point {
+ .text {
+ color: var(--bg-ink-400);
+ }
+
+ .value {
+ color: var(--bg-ink-100);
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/src/container/TraceMetadata/TraceMetadata.tsx b/frontend/src/container/TraceMetadata/TraceMetadata.tsx
new file mode 100644
index 00000000000..042ee654feb
--- /dev/null
+++ b/frontend/src/container/TraceMetadata/TraceMetadata.tsx
@@ -0,0 +1,100 @@
+import './TraceMetadata.styles.scss';
+
+import { Button, Tooltip, Typography } from 'antd';
+import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
+import ROUTES from 'constants/routes';
+import history from 'lib/history';
+import {
+ ArrowLeft,
+ BetweenHorizonalStart,
+ CalendarClock,
+ DraftingCompass,
+ Timer,
+} from 'lucide-react';
+import { formatEpochTimestamp } from 'utils/timeUtils';
+
+export interface ITraceMetadataProps {
+ traceID: string;
+ rootServiceName: string;
+ rootSpanName: string;
+ startTime: number;
+ duration: number;
+ totalSpans: number;
+ totalErrorSpans: number;
+ notFound: boolean;
+}
+
+function TraceMetadata(props: ITraceMetadataProps): JSX.Element {
+ const {
+ traceID,
+ rootServiceName,
+ rootSpanName,
+ startTime,
+ duration,
+ totalErrorSpans,
+ totalSpans,
+ notFound,
+ } = props;
+ return (
+
+
+
+
+ history.push(ROUTES.TRACES_EXPLORER)}
+ />
+
+
+
+ Trace ID
+
+
{traceID}
+
+ {!notFound && (
+
+
+
+ {rootServiceName}
+ —
+
+ {rootSpanName}
+
+
+
+
+
+
+
+ {getYAxisFormattedValue(`${duration}`, 'ms')}
+
+
+
+
+
+
+
+ {formatEpochTimestamp(startTime * 1000)}
+
+
+
+ )}
+
+ {!notFound && (
+
+
+ Total Spans
+ {totalSpans}
+
+
+
+ Error Spans
+ {totalErrorSpans}
+
+
+ )}
+
+ );
+}
+
+export default TraceMetadata;
diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfall.styles.scss b/frontend/src/container/TraceWaterfall/TraceWaterfall.styles.scss
new file mode 100644
index 00000000000..015fc601342
--- /dev/null
+++ b/frontend/src/container/TraceWaterfall/TraceWaterfall.styles.scss
@@ -0,0 +1,9 @@
+.trace-waterfall {
+ height: calc(70vh - 200px);
+
+ .loading-skeleton {
+ justify-content: center;
+ align-items: center;
+ padding: 20px;
+ }
+}
diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfall.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfall.tsx
new file mode 100644
index 00000000000..cd6db42f4f4
--- /dev/null
+++ b/frontend/src/container/TraceWaterfall/TraceWaterfall.tsx
@@ -0,0 +1,136 @@
+import './TraceWaterfall.styles.scss';
+
+import { Skeleton } from 'antd';
+import { AxiosError } from 'axios';
+import Spinner from 'components/Spinner';
+import { Dispatch, SetStateAction, useMemo } from 'react';
+import { ErrorResponse, SuccessResponse } from 'types/api';
+import { GetTraceV2SuccessResponse, Span } from 'types/api/trace/getTraceV2';
+
+import { TraceWaterfallStates } from './constants';
+import Error from './TraceWaterfallStates/Error/Error';
+import NoData from './TraceWaterfallStates/NoData/NoData';
+import Success from './TraceWaterfallStates/Success/Success';
+
+export interface IInterestedSpan {
+ spanId: string;
+ isUncollapsed: boolean;
+}
+
+interface ITraceWaterfallProps {
+ traceId: string;
+ uncollapsedNodes: string[];
+ traceData:
+ | SuccessResponse
+ | ErrorResponse
+ | undefined;
+ isFetchingTraceData: boolean;
+ errorFetchingTraceData: unknown;
+ interestedSpanId: IInterestedSpan;
+ setInterestedSpanId: Dispatch>;
+ setTraceFlamegraphStatsWidth: Dispatch>;
+ selectedSpan: Span | undefined;
+ setSelectedSpan: Dispatch>;
+}
+
+function TraceWaterfall(props: ITraceWaterfallProps): JSX.Element {
+ const {
+ traceData,
+ isFetchingTraceData,
+ errorFetchingTraceData,
+ interestedSpanId,
+ traceId,
+ uncollapsedNodes,
+ setInterestedSpanId,
+ setTraceFlamegraphStatsWidth,
+ setSelectedSpan,
+ selectedSpan,
+ } = props;
+ // get the current state of trace waterfall based on the API lifecycle
+ const traceWaterfallState = useMemo(() => {
+ if (isFetchingTraceData) {
+ if (
+ traceData &&
+ traceData.payload &&
+ traceData.payload.spans &&
+ traceData.payload.spans.length > 0
+ ) {
+ return TraceWaterfallStates.FETCHING_WITH_OLD_DATA_PRESENT;
+ }
+ return TraceWaterfallStates.LOADING;
+ }
+ if (errorFetchingTraceData) {
+ return TraceWaterfallStates.ERROR;
+ }
+ if (
+ traceData &&
+ traceData.payload &&
+ traceData.payload.spans &&
+ traceData.payload.spans.length === 0
+ ) {
+ return TraceWaterfallStates.NO_DATA;
+ }
+
+ return TraceWaterfallStates.SUCCESS;
+ }, [errorFetchingTraceData, isFetchingTraceData, traceData]);
+
+ // capture the spans from the response, since we do not need to do any manipulation on the same we will keep this as a simple constant [ memoized ]
+ const spans = useMemo(() => traceData?.payload?.spans || [], [
+ traceData?.payload?.spans,
+ ]);
+
+ // get the content based on the current state of the trace waterfall
+ const getContent = useMemo(() => {
+ switch (traceWaterfallState) {
+ case TraceWaterfallStates.LOADING:
+ return (
+
+
+
+ );
+ case TraceWaterfallStates.ERROR:
+ return ;
+ case TraceWaterfallStates.NO_DATA:
+ return ;
+ case TraceWaterfallStates.SUCCESS:
+ case TraceWaterfallStates.FETCHING_WITH_OLD_DATA_PRESENT:
+ return (
+
+ );
+ default:
+ return ;
+ }
+ }, [
+ errorFetchingTraceData,
+ interestedSpanId,
+ selectedSpan,
+ setInterestedSpanId,
+ setSelectedSpan,
+ setTraceFlamegraphStatsWidth,
+ spans,
+ traceData?.payload?.endTimestampMillis,
+ traceData?.payload?.hasMissingSpans,
+ traceData?.payload?.startTimestampMillis,
+ traceId,
+ traceWaterfallState,
+ uncollapsedNodes,
+ ]);
+
+ return {getContent}
;
+}
+
+export default TraceWaterfall;
diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Error/Error.styles.scss b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Error/Error.styles.scss
new file mode 100644
index 00000000000..6382b0157a9
--- /dev/null
+++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Error/Error.styles.scss
@@ -0,0 +1,30 @@
+.error-waterfall {
+ display: flex;
+ padding: 12px;
+ margin: 20px;
+ gap: 12px;
+ align-items: flex-start;
+ border-radius: 4px;
+ background: var(--bg-cherry-500);
+
+ .text {
+ color: var(--bg-vanilla-100);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ flex-shrink: 0;
+ }
+
+ .value {
+ color: var(--bg-vanilla-100);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ }
+}
diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Error/Error.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Error/Error.tsx
new file mode 100644
index 00000000000..27b8428c0ef
--- /dev/null
+++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Error/Error.tsx
@@ -0,0 +1,25 @@
+import './Error.styles.scss';
+
+import { Tooltip, Typography } from 'antd';
+import { AxiosError } from 'axios';
+
+interface IErrorProps {
+ error: AxiosError;
+}
+
+function Error(props: IErrorProps): JSX.Element {
+ const { error } = props;
+
+ return (
+
+ Something went wrong!
+
+
+ {error?.message}
+
+
+
+ );
+}
+
+export default Error;
diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/NoData/NoData.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/NoData/NoData.tsx
new file mode 100644
index 00000000000..0be04ffc234
--- /dev/null
+++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/NoData/NoData.tsx
@@ -0,0 +1,12 @@
+import { Typography } from 'antd';
+
+interface INoDataProps {
+ id: string;
+}
+
+function NoData(props: INoDataProps): JSX.Element {
+ const { id } = props;
+ return No Trace found with the id: {id} ;
+}
+
+export default NoData;
diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.styles.scss b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.styles.scss
new file mode 100644
index 00000000000..421a0e6db60
--- /dev/null
+++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.styles.scss
@@ -0,0 +1,60 @@
+.filter-row {
+ display: flex;
+ align-items: center;
+ padding: 16px 20px 0px 20px;
+ gap: 12px;
+
+ .query-builder-search-v2 {
+ width: 100%;
+ }
+
+ .pre-next-toggle {
+ display: flex;
+ flex-shrink: 0;
+ gap: 12px;
+
+ .ant-typography {
+ display: flex;
+ align-items: center;
+ color: var(--bg-vanilla-400);
+ font-family: 'Geist Mono';
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 18px; /* 150% */
+ }
+
+ .ant-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: none;
+ }
+ }
+
+ .no-results {
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+ color: var(--bg-vanilla-400);
+ font-family: 'Geist Mono';
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 18px; /* 150% */
+ }
+}
+
+.lightMode {
+ .filter-row {
+ .pre-next-toggle {
+ .ant-typography {
+ color: var(--bg-ink-400);
+ }
+ }
+
+ .no-results {
+ color: var(--bg-ink-400);
+ }
+ }
+}
diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.tsx
new file mode 100644
index 00000000000..e0bee45465a
--- /dev/null
+++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.tsx
@@ -0,0 +1,181 @@
+import './Filters.styles.scss';
+
+import { InfoCircleOutlined, LoadingOutlined } from '@ant-design/icons';
+import { Button, Spin, Tooltip, Typography } from 'antd';
+import { AxiosError } from 'axios';
+import { DEFAULT_ENTITY_VERSION } from 'constants/app';
+import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
+import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
+import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
+import { ChevronDown, ChevronUp } from 'lucide-react';
+import { useCallback, useEffect, useState } from 'react';
+import { useHistory, useLocation } from 'react-router-dom';
+import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
+import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
+import { TracesAggregatorOperator } from 'types/common/queryBuilder';
+
+import { BASE_FILTER_QUERY } from './constants';
+
+function prepareQuery(filters: TagFilter, traceID: string): Query {
+ return {
+ ...initialQueriesMap.traces,
+ builder: {
+ ...initialQueriesMap.traces.builder,
+ queryData: [
+ {
+ ...initialQueriesMap.traces.builder.queryData[0],
+ aggregateOperator: TracesAggregatorOperator.NOOP,
+ orderBy: [{ columnName: 'timestamp', order: 'asc' }],
+ filters: {
+ ...filters,
+ items: [
+ ...filters.items,
+ {
+ id: '5ab8e1cf',
+ key: {
+ key: 'trace_id',
+ dataType: DataTypes.String,
+ type: '',
+ isColumn: true,
+ isJSON: false,
+ id: 'trace_id--string----true',
+ },
+ op: '=',
+ value: traceID,
+ },
+ ],
+ },
+ },
+ ],
+ },
+ };
+}
+
+function Filters({
+ startTime,
+ endTime,
+ traceID,
+}: {
+ startTime: number;
+ endTime: number;
+ traceID: string;
+}): JSX.Element {
+ const [filters, setFilters] = useState(BASE_FILTER_QUERY.filters);
+ const [noData, setNoData] = useState(false);
+ const [filteredSpanIds, setFilteredSpanIds] = useState([]);
+ const handleFilterChange = (value: TagFilter): void => {
+ setFilters(value);
+ };
+ const [currentSearchedIndex, setCurrentSearchedIndex] = useState(0);
+ const { search } = useLocation();
+ const history = useHistory();
+
+ const { isFetching, error } = useGetQueryRange(
+ {
+ query: prepareQuery(filters, traceID),
+ graphType: PANEL_TYPES.LIST,
+ selectedTime: 'GLOBAL_TIME',
+ start: startTime,
+ end: endTime,
+ params: {
+ dataSource: 'traces',
+ },
+ tableParams: {
+ pagination: {
+ offset: 0,
+ limit: 200,
+ },
+ selectColumns: [
+ {
+ key: 'name',
+ dataType: 'string',
+ type: 'tag',
+ isColumn: true,
+ isJSON: false,
+ id: 'name--string--tag--true',
+ isIndexed: false,
+ },
+ ],
+ },
+ },
+ DEFAULT_ENTITY_VERSION,
+ {
+ queryKey: [filters],
+ enabled: filters.items.length > 0,
+ onSuccess: (data) => {
+ if (data?.payload.data.newResult.data.result[0].list) {
+ setFilteredSpanIds(
+ data?.payload.data.newResult.data.result[0].list.map(
+ (val) => val.data.spanID,
+ ),
+ );
+ setNoData(false);
+ } else {
+ setNoData(true);
+ setFilteredSpanIds([]);
+ setCurrentSearchedIndex(0);
+ }
+ },
+ },
+ );
+
+ const handlePrevNext = useCallback(
+ (id: number): void => {
+ const searchParams = new URLSearchParams(search);
+ searchParams.set('spanId', filteredSpanIds[id]);
+ history.replace({ search: searchParams.toString() });
+ },
+ [filteredSpanIds, history, search],
+ );
+
+ useEffect(() => {
+ if (filteredSpanIds.length > 0) {
+ handlePrevNext(0);
+ }
+ }, [filteredSpanIds, handlePrevNext]);
+
+ return (
+
+
+ {filteredSpanIds.length > 0 && (
+
+
+ {currentSearchedIndex + 1} / {filteredSpanIds.length}
+
+ }
+ disabled={currentSearchedIndex === 0}
+ type="text"
+ onClick={(): void => {
+ handlePrevNext(currentSearchedIndex - 1);
+ setCurrentSearchedIndex((prev) => prev - 1);
+ }}
+ />
+ }
+ type="text"
+ disabled={currentSearchedIndex === filteredSpanIds.length - 1}
+ onClick={(): void => {
+ handlePrevNext(currentSearchedIndex + 1);
+ setCurrentSearchedIndex((prev) => prev + 1);
+ }}
+ />
+
+ )}
+ {isFetching &&
} size="small" />}
+ {error && (
+
+
+
+ )}
+ {noData && (
+
No results found
+ )}
+
+ );
+}
+
+export default Filters;
diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/constants.ts b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/constants.ts
new file mode 100644
index 00000000000..aeaa46baef4
--- /dev/null
+++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/constants.ts
@@ -0,0 +1,40 @@
+import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
+import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
+import { DataSource } from 'types/common/queryBuilder';
+
+export const BASE_FILTER_QUERY: IBuilderQuery = {
+ queryName: 'A',
+ dataSource: DataSource.TRACES,
+ aggregateOperator: 'noop',
+ aggregateAttribute: {
+ id: '------false',
+ dataType: DataTypes.EMPTY,
+ key: '',
+ isColumn: false,
+ type: '',
+ isJSON: false,
+ },
+ timeAggregation: 'rate',
+ spaceAggregation: 'sum',
+ functions: [],
+ filters: {
+ items: [],
+ op: 'AND',
+ },
+ expression: 'A',
+ disabled: false,
+ stepInterval: 60,
+ having: [],
+ limit: 200,
+ orderBy: [
+ {
+ columnName: 'timestamp',
+ order: 'desc',
+ },
+ ],
+ groupBy: [],
+ legend: '',
+ reduceTo: 'avg',
+ offset: 0,
+ selectColumns: [],
+};
diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.styles.scss b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.styles.scss
new file mode 100644
index 00000000000..1b33cff2954
--- /dev/null
+++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.styles.scss
@@ -0,0 +1,396 @@
+.success-content {
+ overflow-y: hidden;
+ overflow-x: hidden;
+ max-width: 100%;
+
+ .missing-spans {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 44px;
+ margin: 16px;
+ padding: 12px;
+ border-radius: 4px;
+ background: rgba(69, 104, 220, 0.1);
+
+ .left-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--bg-robin-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+
+ .text {
+ color: var(--bg-robin-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ }
+ }
+
+ .right-info {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: row-reverse;
+ gap: 8px;
+ color: var(--bg-robin-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ }
+
+ .right-info:hover {
+ background-color: unset;
+ color: var(--bg-robin-200);
+ }
+ }
+
+ .waterfall-table {
+ height: calc(70vh - 220px);
+ overflow: auto;
+ overflow-x: hidden;
+ padding: 0px 20px 20px 20px;
+
+ &::-webkit-scrollbar {
+ width: 0.1rem;
+ }
+
+ // default table overrides css for table v3
+ .div-table {
+ width: 100% !important;
+ border: none !important;
+ }
+
+ .div-thead {
+ position: sticky;
+ top: 0;
+ z-index: 2;
+ background-color: var(--bg-ink-500) !important;
+
+ .div-tr {
+ height: 16px;
+ }
+ }
+
+ .div-tr {
+ display: flex;
+ width: 100%;
+ align-items: center;
+ height: 54px;
+ }
+
+ .div-tr:hover {
+ border-radius: 4px;
+ background: rgba(171, 189, 255, 0.06) !important;
+
+ .span-overview {
+ background: unset !important;
+
+ .span-overview-content {
+ background: unset !important;
+ }
+ }
+ }
+
+ .div-th,
+ .div-td {
+ box-shadow: none;
+ padding: 0px !important;
+ }
+
+ .div-th {
+ padding: 2px 4px;
+ position: relative;
+ font-weight: bold;
+ text-align: center;
+ }
+
+ .div-td {
+ display: flex;
+ height: 54px;
+ align-items: center;
+ overflow: hidden;
+
+ .span-overview {
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+ height: 100%;
+ width: 100%;
+ cursor: pointer;
+
+ .connector-lines {
+ display: flex;
+ }
+
+ .span-overview-content {
+ display: flex;
+ flex-shrink: 0;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 5px;
+ width: 100%;
+ background-color: #0b0c0e;
+ height: 100%;
+ justify-content: center;
+
+ .first-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 20px;
+ width: 100%;
+
+ .span-det {
+ display: flex;
+ gap: 6px;
+ flex-shrink: 0;
+
+ .collapse-uncollapse-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 4px 4px;
+ gap: 4px;
+ border-radius: 2px;
+ border: 1px solid var(--bg-slate-400);
+ background: var(--bg-slate-500);
+ box-shadow: none;
+ height: 20px;
+
+ .children-count {
+ color: var(--bg-vanilla-400);
+ font-family: 'Space Mono';
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: 0.28px;
+ }
+ }
+
+ .span-name {
+ color: #fff;
+ font-family: 'Inter';
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: 0.28px;
+ }
+ }
+
+ .status-code-container {
+ display: flex;
+ padding-right: 10px;
+
+ .status-code {
+ display: flex;
+ height: 20px;
+ padding: 3px;
+ align-items: center;
+ border-radius: 3px;
+ }
+
+ .success {
+ border: 1px solid var(--bg-robin-500);
+ background: var(--bg-robin-500);
+ }
+
+ .error {
+ border: 1px solid var(--bg-cherry-500);
+ background: var(--bg-cherry-500);
+ }
+ }
+ }
+
+ .second-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ height: 18px;
+ width: 100%;
+ .service-name {
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 18px; /* 128.571% */
+ letter-spacing: -0.07px;
+ }
+ }
+ }
+ }
+
+ .span-duration {
+ display: flex;
+ flex-direction: column;
+ height: 54px;
+ position: relative;
+ width: 100%;
+ cursor: pointer;
+
+ .span-line {
+ position: absolute;
+ height: 12px;
+ top: 35%;
+ border-radius: 6px;
+ }
+
+ .span-line-text {
+ position: absolute;
+ top: 65%;
+ font-variant-numeric: lining-nums tabular-nums stacked-fractions
+ slashed-zero;
+ font-feature-settings: 'case' on, 'cpsp' on, 'dlig' on, 'salt' on;
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 18px; /* 128.571% */
+ letter-spacing: -0.07px;
+ }
+ }
+
+ .interested-span {
+ border-radius: 4px;
+ background: rgba(171, 189, 255, 0.06) !important;
+
+ .span-overview-content {
+ background: unset;
+ }
+ }
+ }
+
+ .div-td + .div-td {
+ border-left: 1px solid var(--bg-slate-400);
+ }
+
+ .div-th + .div-th {
+ border-left: 1px solid var(--bg-slate-400);
+ }
+
+ .div-tr .div-th:nth-child(2) {
+ width: calc(100% - var(--header-span-name-size) * 1px) !important;
+ }
+ .div-tr .div-td:nth-child(2) {
+ width: calc(100% - var(--header-span-name-size) * 1px) !important;
+ }
+ .resizer {
+ width: 10px !important;
+ position: absolute;
+ top: 0;
+ height: calc(70vh - 200px);
+ right: 0;
+ width: 2px;
+ background: rgba(35, 196, 248, 0.2);
+ cursor: col-resize;
+ user-select: none;
+ touch-action: none;
+ }
+
+ .resizer.isResizing {
+ background: rgba(35, 196, 248, 0.2);
+ opacity: 1;
+ }
+
+ @media (hover: hover) {
+ .resizer {
+ opacity: 0;
+ }
+
+ *:hover > .resizer {
+ opacity: 1;
+ }
+ }
+ }
+
+ .missing-spans-waterfall-table {
+ height: calc(70vh - 280px);
+ }
+}
+
+.span-dets {
+ .related-logs {
+ display: flex;
+ width: 160px;
+ padding: 4px 8px;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ border-radius: 2px;
+ border: 1px solid var(--Slate-400, #1d212d);
+ background: var(--Slate-500, #161922);
+ box-shadow: none;
+ }
+}
+
+.lightMode {
+ .success-content {
+ .waterfall-table {
+ .div-td {
+ .span-overview {
+ .span-overview-content {
+ background-color: var(--bg-vanilla-200);
+ .first-row {
+ .collapse-uncollapse-button {
+ border: 1px solid var(--bg-vanilla-400);
+ background: var(--bg-vanilla-400);
+
+ .children-count {
+ color: var(--bg-ink-400);
+ }
+ }
+
+ .span-name {
+ color: var(--bg-ink-400);
+ }
+ }
+
+ .second-row {
+ .service-name {
+ color: var(--bg-ink-400);
+ }
+ }
+ }
+ }
+
+ .interested-span {
+ border-radius: 4px;
+ background: var(--bg-vanilla-300);
+ }
+ }
+
+ .div-td + .div-td {
+ border-left: 1px solid var(--bg-vanilla-300);
+ }
+
+ .div-th + .div-th {
+ border-left: 1px solid var(--bg-vanilla-300);
+ }
+
+ .div-thead {
+ background-color: var(--bg-vanilla-200) !important;
+ }
+ }
+ }
+ .span-dets {
+ .related-logs {
+ border: 1px solid var(--bg-vanilla-300);
+ background: var(--bg-vanilla-300);
+ }
+ }
+}
diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx
new file mode 100644
index 00000000000..73cd7b0e16f
--- /dev/null
+++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx
@@ -0,0 +1,381 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+import './Success.styles.scss';
+
+import { ColumnDef, createColumnHelper } from '@tanstack/react-table';
+import { Virtualizer } from '@tanstack/react-virtual';
+import { Button, Tooltip, Typography } from 'antd';
+import cx from 'classnames';
+import { TableV3 } from 'components/TableV3/TableV3';
+import { themeColors } from 'constants/theme';
+import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
+import { IInterestedSpan } from 'container/TraceWaterfall/TraceWaterfall';
+import { generateColor } from 'lib/uPlotLib/utils/generateColor';
+import {
+ AlertCircle,
+ ArrowUpRight,
+ ChevronDown,
+ ChevronRight,
+ Leaf,
+} from 'lucide-react';
+import {
+ Dispatch,
+ SetStateAction,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+} from 'react';
+import { Span } from 'types/api/trace/getTraceV2';
+import { toFixed } from 'utils/toFixed';
+
+import Filters from './Filters/Filters';
+
+// css config
+const CONNECTOR_WIDTH = 28;
+const VERTICAL_CONNECTOR_WIDTH = 1;
+
+interface ITraceMetadata {
+ traceId: string;
+ startTime: number;
+ endTime: number;
+ hasMissingSpans: boolean;
+}
+interface ISuccessProps {
+ spans: Span[];
+ traceMetadata: ITraceMetadata;
+ interestedSpanId: IInterestedSpan;
+ uncollapsedNodes: string[];
+ setInterestedSpanId: Dispatch>;
+ setTraceFlamegraphStatsWidth: Dispatch>;
+ selectedSpan: Span | undefined;
+ setSelectedSpan: Dispatch>;
+}
+
+function SpanOverview({
+ span,
+ isSpanCollapsed,
+ handleCollapseUncollapse,
+ setSelectedSpan,
+ selectedSpan,
+}: {
+ span: Span;
+ isSpanCollapsed: boolean;
+ handleCollapseUncollapse: (id: string, collapse: boolean) => void;
+ selectedSpan: Span | undefined;
+ setSelectedSpan: Dispatch>;
+}): JSX.Element {
+ const isRootSpan = span.level === 0;
+ const spanRef = useRef(null);
+
+ let color = generateColor(span.serviceName, themeColors.traceDetailColors);
+ if (span.hasError) {
+ color = `var(--bg-cherry-500)`;
+ }
+
+ return (
+ ')`,
+ backgroundRepeat: 'repeat',
+ backgroundSize: `${CONNECTOR_WIDTH + 1}px 54px`,
+ }}
+ onClick={(): void => {
+ setSelectedSpan(span);
+ }}
+ >
+ {!isRootSpan && (
+
+ )}
+
+
+
+ {span.hasChildren ? (
+ {
+ event.stopPropagation();
+ event.preventDefault();
+ handleCollapseUncollapse(span.spanId, !isSpanCollapsed);
+ }}
+ className="collapse-uncollapse-button"
+ >
+ {isSpanCollapsed ? (
+
+ ) : (
+
+ )}
+
+ {span.subTreeNodeCount}
+
+
+ ) : (
+
+
+
+ )}
+ {span.name}
+
+
+
+
+
+ {span.serviceName}
+
+
+
+
+ );
+}
+
+function SpanDuration({
+ span,
+ traceMetadata,
+ setSelectedSpan,
+ selectedSpan,
+}: {
+ span: Span;
+ traceMetadata: ITraceMetadata;
+ selectedSpan: Span | undefined;
+ setSelectedSpan: Dispatch>;
+}): JSX.Element {
+ const { time, timeUnitName } = convertTimeToRelevantUnit(
+ span.durationNano / 1e6,
+ );
+
+ const spread = traceMetadata.endTime - traceMetadata.startTime;
+ const leftOffset = ((span.timestamp - traceMetadata.startTime) * 1e2) / spread;
+ const width = (span.durationNano * 1e2) / (spread * 1e6);
+
+ let color = generateColor(span.serviceName, themeColors.traceDetailColors);
+
+ if (span.hasError) {
+ color = `var(--bg-cherry-500)`;
+ }
+
+ return (
+ {
+ setSelectedSpan(span);
+ }}
+ >
+
+
+ {`${toFixed(time, 2)} ${timeUnitName}`}
+
+
+ );
+}
+
+// table config
+const columnDefHelper = createColumnHelper();
+
+function getWaterfallColumns({
+ handleCollapseUncollapse,
+ uncollapsedNodes,
+ traceMetadata,
+ selectedSpan,
+ setSelectedSpan,
+}: {
+ handleCollapseUncollapse: (id: string, collapse: boolean) => void;
+ uncollapsedNodes: string[];
+ traceMetadata: ITraceMetadata;
+ selectedSpan: Span | undefined;
+ setSelectedSpan: Dispatch>;
+}): ColumnDef[] {
+ const waterfallColumns: ColumnDef[] = [
+ columnDefHelper.display({
+ id: 'span-name',
+ header: '',
+ cell: (props): JSX.Element => (
+
+ ),
+ size: 450,
+ }),
+ columnDefHelper.display({
+ id: 'span-duration',
+ header: () =>
,
+ enableResizing: false,
+ cell: (props): JSX.Element => (
+
+ ),
+ }),
+ ];
+
+ return waterfallColumns;
+}
+
+function Success(props: ISuccessProps): JSX.Element {
+ const {
+ spans,
+ traceMetadata,
+ interestedSpanId,
+ uncollapsedNodes,
+ setInterestedSpanId,
+ setTraceFlamegraphStatsWidth,
+ setSelectedSpan,
+ selectedSpan,
+ } = props;
+ const virtualizerRef = useRef>();
+
+ const handleCollapseUncollapse = useCallback(
+ (spanId: string, collapse: boolean) => {
+ setInterestedSpanId({ spanId, isUncollapsed: !collapse });
+ },
+ [setInterestedSpanId],
+ );
+
+ const handleVirtualizerInstanceChanged = (
+ instance: Virtualizer,
+ ): void => {
+ const { range } = instance;
+ if (spans.length < 500) return;
+
+ if (range?.startIndex === 0 && instance.isScrolling) {
+ if (spans[0].level !== 0) {
+ setInterestedSpanId({ spanId: spans[0].spanId, isUncollapsed: false });
+ }
+ return;
+ }
+
+ if (range?.endIndex === spans.length - 1 && instance.isScrolling) {
+ setInterestedSpanId({
+ spanId: spans[spans.length - 1].spanId,
+ isUncollapsed: false,
+ });
+ }
+ };
+
+ const columns = useMemo(
+ () =>
+ getWaterfallColumns({
+ handleCollapseUncollapse,
+ uncollapsedNodes,
+ traceMetadata,
+ selectedSpan,
+ setSelectedSpan,
+ }),
+ [
+ handleCollapseUncollapse,
+ uncollapsedNodes,
+ traceMetadata,
+ selectedSpan,
+ setSelectedSpan,
+ ],
+ );
+
+ useEffect(() => {
+ if (interestedSpanId.spanId !== '' && virtualizerRef.current) {
+ const idx = spans.findIndex(
+ (span) => span.spanId === interestedSpanId.spanId,
+ );
+ if (idx !== -1) {
+ setTimeout(() => {
+ virtualizerRef.current?.scrollToIndex(idx, {
+ align: 'center',
+ behavior: 'auto',
+ });
+ }, 400);
+
+ setSelectedSpan(spans[idx]);
+ }
+ } else {
+ setSelectedSpan((prev) => {
+ if (!prev) {
+ return spans[0];
+ }
+ return prev;
+ });
+ }
+ }, [interestedSpanId, setSelectedSpan, spans]);
+
+ return (
+
+ {traceMetadata.hasMissingSpans && (
+
+
+
+
+ This trace has missing spans
+
+
+
}
+ className="right-info"
+ type="text"
+ >
+ Learn More
+
+
+ )}
+
+
+
+ );
+}
+
+export default Success;
diff --git a/frontend/src/container/TraceWaterfall/constants.ts b/frontend/src/container/TraceWaterfall/constants.ts
new file mode 100644
index 00000000000..e9056512f4f
--- /dev/null
+++ b/frontend/src/container/TraceWaterfall/constants.ts
@@ -0,0 +1,7 @@
+export enum TraceWaterfallStates {
+ LOADING = 'LOADING',
+ SUCCESS = 'SUCCESS',
+ NO_DATA = 'NO_DATA',
+ ERROR = 'ERROR',
+ FETCHING_WITH_OLD_DATA_PRESENT = 'FETCHING_WTIH_OLD_DATA_PRESENT',
+}
diff --git a/frontend/src/hooks/trace/useGetTraceFlamegraph.tsx b/frontend/src/hooks/trace/useGetTraceFlamegraph.tsx
new file mode 100644
index 00000000000..a7444d3a20e
--- /dev/null
+++ b/frontend/src/hooks/trace/useGetTraceFlamegraph.tsx
@@ -0,0 +1,31 @@
+import getTraceFlamegraph from 'api/trace/getTraceFlamegraph';
+import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
+import { useQuery, UseQueryResult } from 'react-query';
+import { ErrorResponse, SuccessResponse } from 'types/api';
+import {
+ GetTraceFlamegraphPayloadProps,
+ GetTraceFlamegraphSuccessResponse,
+} from 'types/api/trace/getTraceFlamegraph';
+
+const useGetTraceFlamegraph = (
+ props: GetTraceFlamegraphPayloadProps,
+): UseLicense =>
+ useQuery({
+ queryFn: () => getTraceFlamegraph(props),
+ // if any of the props changes then we need to trigger an API call as the older data will be obsolete
+ queryKey: [
+ REACT_QUERY_KEY.GET_TRACE_V2_FLAMEGRAPH,
+ props.traceId,
+ props.selectedSpanId,
+ ],
+ enabled: !!props.traceId,
+ keepPreviousData: true,
+ refetchOnWindowFocus: false,
+ });
+
+type UseLicense = UseQueryResult<
+ SuccessResponse | ErrorResponse,
+ unknown
+>;
+
+export default useGetTraceFlamegraph;
diff --git a/frontend/src/hooks/trace/useGetTraceV2.tsx b/frontend/src/hooks/trace/useGetTraceV2.tsx
new file mode 100644
index 00000000000..90b821a187d
--- /dev/null
+++ b/frontend/src/hooks/trace/useGetTraceV2.tsx
@@ -0,0 +1,30 @@
+import getTraceV2 from 'api/trace/getTraceV2';
+import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
+import { useQuery, UseQueryResult } from 'react-query';
+import { ErrorResponse, SuccessResponse } from 'types/api';
+import {
+ GetTraceV2PayloadProps,
+ GetTraceV2SuccessResponse,
+} from 'types/api/trace/getTraceV2';
+
+const useGetTraceV2 = (props: GetTraceV2PayloadProps): UseLicense =>
+ useQuery({
+ queryFn: () => getTraceV2(props),
+ // if any of the props changes then we need to trigger an API call as the older data will be obsolete
+ queryKey: [
+ REACT_QUERY_KEY.GET_TRACE_V2_WATERFALL,
+ props.traceId,
+ props.selectedSpanId,
+ props.isSelectedSpanIDUnCollapsed,
+ ],
+ enabled: !!props.traceId,
+ keepPreviousData: true,
+ refetchOnWindowFocus: false,
+ });
+
+type UseLicense = UseQueryResult<
+ SuccessResponse | ErrorResponse,
+ unknown
+>;
+
+export default useGetTraceV2;
diff --git a/frontend/src/pages/TraceDetail/TraceDetail.styles.scss b/frontend/src/pages/TraceDetail/TraceDetail.styles.scss
new file mode 100644
index 00000000000..bf1696ebe4d
--- /dev/null
+++ b/frontend/src/pages/TraceDetail/TraceDetail.styles.scss
@@ -0,0 +1,45 @@
+.old-trace-container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ .top-header {
+ display: flex;
+ flex-direction: row-reverse;
+ padding: 5px;
+ border-bottom: 1px solid var(--bg-slate-400);
+
+ .new-cta-btn {
+ display: flex;
+ padding: 4px 6px;
+ align-items: center;
+ gap: 8px;
+ color: var(--Vanilla-400, #c0c1c3);
+
+ /* Bifrost (Ancient)/Content/sm */
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ box-shadow: none;
+
+ .ant-btn-icon {
+ margin-inline-end: 0px;
+ }
+ }
+ }
+}
+
+.lightMode {
+ .old-trace-container {
+ .top-header {
+ border-bottom: 1px solid var(--bg-vanilla-300);
+
+ .new-cta-btn {
+ color: var(--bg-ink-400);
+ }
+ }
+ }
+}
diff --git a/frontend/src/pages/TraceDetail/index.tsx b/frontend/src/pages/TraceDetail/index.tsx
index f936bd2d0c0..3ab48562675 100644
--- a/frontend/src/pages/TraceDetail/index.tsx
+++ b/frontend/src/pages/TraceDetail/index.tsx
@@ -1,10 +1,14 @@
-import { Typography } from 'antd';
+import './TraceDetail.styles.scss';
+
+import { Button, Typography } from 'antd';
import getTraceItem from 'api/trace/getTraceItem';
import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner';
import TraceDetailContainer from 'container/TraceDetail';
import useUrlQuery from 'hooks/useUrlQuery';
-import { useMemo } from 'react';
+import { Undo } from 'lucide-react';
+import TraceDetailsPage from 'pages/TraceDetailV2';
+import { useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useParams } from 'react-router-dom';
import { Props as TraceDetailProps } from 'types/api/trace/getTraceItem';
@@ -13,6 +17,7 @@ import { noEventMessage } from './constants';
function TraceDetail(): JSX.Element {
const { id } = useParams();
+ const [showNewTraceDetails, setShowNewTraceDetails] = useState(false);
const urlQuery = useUrlQuery();
const { spanId, levelUp, levelDown } = useMemo(
() => ({
@@ -31,6 +36,10 @@ function TraceDetail(): JSX.Element {
},
);
+ if (showNewTraceDetails) {
+ return ;
+ }
+
if (traceDetailResponse?.error || error || isError) {
return (
@@ -47,7 +56,21 @@ function TraceDetail(): JSX.Element {
return ;
}
- return ;
+ return (
+
+
+ setShowNewTraceDetails(true)}
+ icon={ }
+ type="text"
+ className="new-cta-btn"
+ >
+ New Trace Detail
+
+
+
;
+
+ );
}
export default TraceDetail;
diff --git a/frontend/src/pages/TraceDetailV2/NoData/NoData.styles.scss b/frontend/src/pages/TraceDetailV2/NoData/NoData.styles.scss
new file mode 100644
index 00000000000..f674b3ea97e
--- /dev/null
+++ b/frontend/src/pages/TraceDetailV2/NoData/NoData.styles.scss
@@ -0,0 +1,167 @@
+.not-found-trace {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 60vh;
+ width: 500px;
+ gap: 24px;
+ margin: 0 auto;
+
+ .description {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+
+ .not-found-img {
+ height: 32px;
+ width: 32px;
+ }
+
+ .not-found-text-1 {
+ color: var(--Vanilla-400, #c0c1c3);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ .not-found-text-2 {
+ color: var(--Vanilla-100, #fff);
+ }
+ }
+ }
+
+ .reasons {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+
+ .reason-1 {
+ display: flex;
+ padding: 12px;
+ align-items: flex-start;
+ gap: 12px;
+ border-radius: 4px;
+ background: rgba(171, 189, 255, 0.04);
+
+ .construction-img {
+ height: 16px;
+ width: 16px;
+ }
+
+ .text {
+ color: var(--Vanilla-400, #c0c1c3);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ }
+ }
+ .reason-2 {
+ display: flex;
+ padding: 12px;
+ align-items: flex-start;
+ gap: 12px;
+ border-radius: 4px;
+ background: rgba(171, 189, 255, 0.04);
+
+ .broom-img {
+ height: 16px;
+ width: 16px;
+ }
+
+ .text {
+ color: var(--Vanilla-400, #c0c1c3);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ }
+ }
+ }
+
+ .none-of-above {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+
+ .text {
+ color: var(--Vanilla-400, #c0c1c3);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ }
+
+ .action-btns {
+ display: flex;
+ gap: 8px;
+
+ .action-btn {
+ display: flex;
+ width: 160px;
+ padding: 4px 8px;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ border-radius: 2px;
+ border: 1px solid var(--Slate-400, #1d212d);
+ background: var(--Slate-500, #161922);
+ box-shadow: none;
+
+ .ant-btn-icon {
+ margin-inline-end: 0px;
+ }
+ }
+ }
+ }
+}
+
+.lightMode {
+ .not-found-trace {
+ .description {
+ .not-found-text-1 {
+ color: var(--bg-ink-400);
+ .not-found-text-2 {
+ color: var(--bg-ink-100);
+ }
+ }
+ }
+
+ .reasons {
+ .reason-1 {
+ background: var(--bg-vanilla-300);
+ .text {
+ color: var(--bg-ink-400);
+ }
+ }
+ .reason-2 {
+ background: var(--bg-vanilla-300);
+
+ .text {
+ color: var(--bg-ink-400);
+ }
+ }
+ }
+
+ .none-of-above {
+ .text {
+ color: var(--bg-ink-400);
+ }
+
+ .action-btns {
+ .action-btn {
+ border: 1px solid var(--bg-vanilla-300);
+ background: var(--bg-vanilla-300);
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/src/pages/TraceDetailV2/NoData/NoData.tsx b/frontend/src/pages/TraceDetailV2/NoData/NoData.tsx
new file mode 100644
index 00000000000..990981f5214
--- /dev/null
+++ b/frontend/src/pages/TraceDetailV2/NoData/NoData.tsx
@@ -0,0 +1,65 @@
+import './NoData.styles.scss';
+
+import { Button, Typography } from 'antd';
+import { LifeBuoy, RefreshCw } from 'lucide-react';
+import { handleContactSupport } from 'pages/Integrations/utils';
+import { isCloudUser } from 'utils/app';
+
+function NoData(): JSX.Element {
+ const isCloudUserVal = isCloudUser();
+ return (
+
+
+
+
+ Uh-oh! We cannot show the selected trace.
+
+ This can happen in either of the two scenraios -
+
+
+
+
+
+
+
+ The trace data has not been rendered on your SigNoz server yet. You can
+ wait for a bit and refresh this page if this is the case.
+
+
+
+
+
+ The trace has been deleted as the data has crossed it’s retention period.
+
+
+
+
+
+ If you feel the issue is none of the above, please contact support.
+
+
+ }
+ onClick={(): void => window.location.reload()}
+ >
+ Refresh this page
+
+ }
+ onClick={(): void => handleContactSupport(isCloudUserVal)}
+ >
+ Contact Support
+
+
+
+
+ );
+}
+
+export default NoData;
diff --git a/frontend/src/pages/TraceDetailV2/TraceDetailV2.styles.scss b/frontend/src/pages/TraceDetailV2/TraceDetailV2.styles.scss
new file mode 100644
index 00000000000..c84884fcf33
--- /dev/null
+++ b/frontend/src/pages/TraceDetailV2/TraceDetailV2.styles.scss
@@ -0,0 +1,207 @@
+.traces-module-container {
+ .trace-module {
+ .ant-tabs-tab {
+ .tab-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ }
+ }
+
+ .ant-tabs-tab-active {
+ .tab-item {
+ color: var(--bg-vanilla-100);
+ }
+ }
+ .ant-tabs-nav {
+ margin: 0px;
+ padding: 0px !important;
+ }
+
+ .ant-tabs-nav::before {
+ border-bottom: 1px solid var(--bg-slate-400) !important;
+ }
+
+ .ant-tabs-nav-list {
+ transform: translate(15px, 0px) !important;
+ }
+ }
+
+ .old-switch {
+ display: flex;
+ align-items: center;
+ color: var(--Vanilla-400, #c0c1c3);
+
+ /* Bifrost (Ancient)/Content/sm */
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+
+ .ant-btn-icon {
+ margin-inline-end: 0px;
+ }
+ }
+
+ .trace-layout {
+ display: flex;
+ height: calc(100vh - 44px);
+
+ .trace-left-content {
+ display: flex;
+ flex-direction: column;
+ gap: 25px;
+ padding-top: 16px;
+
+ .flamegraph-waterfall-toggle {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+ justify-content: center;
+ height: 31px;
+ color: var(--bg-vanilla-400);
+ padding: 5px 20px;
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+
+ .ant-btn-icon {
+ margin-inline-end: 0px !important;
+ }
+ }
+
+ .span-list-toggle {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+ justify-content: center;
+ height: 31px;
+ padding: 5px 20px;
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+
+ .ant-btn-icon {
+ margin-inline-end: 0px !important;
+ }
+ }
+
+ .trace-visualisation-tabs {
+ .ant-tabs-tab {
+ border-radius: 2px 0px 0px 0px;
+ background: var(--bg-ink-400);
+ border-radius: 2px 2px 0px 0px;
+ border: 1px solid var(--bg-slate-400);
+ box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
+ height: 31px;
+ }
+
+ .ant-tabs-tab-active {
+ background-color: var(--bg-ink-500);
+
+ .ant-btn {
+ color: var(--bg-vanilla-100) !important;
+ }
+ }
+
+ .ant-tabs-tab + .ant-tabs-tab {
+ margin: 0px;
+ border-left: 0px;
+ }
+
+ .ant-tabs-ink-bar {
+ height: 1px !important;
+ background: var(--bg-ink-500) !important;
+ }
+
+ .ant-tabs-nav-list {
+ transform: translate(15px, 0px) !important;
+ }
+
+ .ant-tabs-nav::before {
+ border-bottom: 1px solid var(--bg-slate-400);
+ }
+
+ .ant-tabs-nav {
+ margin: 0px;
+ padding: 0px !important;
+ }
+ }
+ }
+ }
+}
+
+.lightMode {
+ .traces-module-container {
+ .trace-module {
+ .ant-tabs-tab {
+ .tab-item {
+ color: var(--bg-ink-400);
+ }
+ }
+
+ .ant-tabs-tab-active {
+ .tab-item {
+ color: var(--bg-ink-100);
+ }
+ }
+
+ .ant-tabs-nav::before {
+ border-bottom: 1px solid var(--bg-vanilla-300) !important;
+ }
+ }
+ .old-switch {
+ color: var(--bg-ink-400);
+ }
+
+ .trace-layout {
+ .flamegraph-waterfall-toggle {
+ color: var(--bg-ink-400);
+ }
+
+ .span-list-toggle {
+ color: var(--bg-ink-400);
+ }
+
+ .trace-visualisation-tabs {
+ .ant-tabs-tab {
+ background: var(--bg-vanilla-100);
+ border: 1px solid var(--bg-vanilla-300);
+ }
+
+ .ant-tabs-tab-active {
+ background-color: var(--bg-vanilla-200);
+
+ .ant-btn {
+ color: var(--bg-ink-100) !important;
+ }
+ }
+
+ .ant-tabs-ink-bar {
+ height: 1px !important;
+ background: var(--bg-vanilla-200) !important;
+ }
+
+ .ant-tabs-nav::before {
+ border-bottom: 1px solid var(--bg-vanilla-300);
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/src/pages/TraceDetailV2/TraceDetailV2.tsx b/frontend/src/pages/TraceDetailV2/TraceDetailV2.tsx
new file mode 100644
index 00000000000..726eeebcde4
--- /dev/null
+++ b/frontend/src/pages/TraceDetailV2/TraceDetailV2.tsx
@@ -0,0 +1,154 @@
+import './TraceDetailV2.styles.scss';
+
+import { Button, Tabs } from 'antd';
+import FlamegraphImg from 'assets/TraceDetail/Flamegraph';
+import TraceFlamegraph from 'container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph';
+import SpanDetailsDrawer from 'container/SpanDetailsDrawer/SpanDetailsDrawer';
+import TraceMetadata from 'container/TraceMetadata/TraceMetadata';
+import TraceWaterfall, {
+ IInterestedSpan,
+} from 'container/TraceWaterfall/TraceWaterfall';
+import useGetTraceV2 from 'hooks/trace/useGetTraceV2';
+import useUrlQuery from 'hooks/useUrlQuery';
+import { defaultTo } from 'lodash-es';
+import { useEffect, useMemo, useState } from 'react';
+import { useParams } from 'react-router-dom';
+import { Span, TraceDetailV2URLProps } from 'types/api/trace/getTraceV2';
+
+import NoData from './NoData/NoData';
+
+function TraceDetailsV2(): JSX.Element {
+ const { id: traceId } = useParams();
+ const urlQuery = useUrlQuery();
+ const [interestedSpanId, setInterestedSpanId] = useState(
+ () => ({
+ spanId: urlQuery.get('spanId') || '',
+ isUncollapsed: urlQuery.get('spanId') !== '',
+ }),
+ );
+ const [
+ traceFlamegraphStatsWidth,
+ setTraceFlamegraphStatsWidth,
+ ] = useState(450);
+ const [isSpanDetailsDocked, setIsSpanDetailsDocked] = useState(false);
+ const [selectedSpan, setSelectedSpan] = useState();
+
+ useEffect(() => {
+ setInterestedSpanId({
+ spanId: urlQuery.get('spanId') || '',
+ isUncollapsed: urlQuery.get('spanId') !== '',
+ });
+ }, [urlQuery]);
+
+ const [uncollapsedNodes, setUncollapsedNodes] = useState([]);
+ const {
+ data: traceData,
+ isFetching: isFetchingTraceData,
+ error: errorFetchingTraceData,
+ } = useGetTraceV2({
+ traceId,
+ uncollapsedSpans: uncollapsedNodes,
+ selectedSpanId: interestedSpanId.spanId,
+ isSelectedSpanIDUnCollapsed: interestedSpanId.isUncollapsed,
+ });
+
+ useEffect(() => {
+ if (traceData && traceData.payload && traceData.payload.uncollapsedSpans) {
+ setUncollapsedNodes(traceData.payload.uncollapsedSpans);
+ }
+ }, [traceData]);
+
+ useEffect(() => {
+ if (selectedSpan) {
+ setIsSpanDetailsDocked(false);
+ }
+ }, [selectedSpan]);
+
+ const noData = useMemo(
+ () =>
+ !isFetchingTraceData &&
+ !errorFetchingTraceData &&
+ defaultTo(traceData?.payload?.spans.length, 0) === 0,
+ [
+ errorFetchingTraceData,
+ isFetchingTraceData,
+ traceData?.payload?.spans.length,
+ ],
+ );
+
+ const items = [
+ {
+ label: (
+ }
+ className="flamegraph-waterfall-toggle"
+ >
+ Flamegraph
+
+ ),
+ key: 'flamegraph',
+ children: (
+ <>
+
+
+ >
+ ),
+ },
+ ];
+
+ return (
+
+
+
+ {!noData ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
+
+export default TraceDetailsV2;
diff --git a/frontend/src/pages/TraceDetailV2/index.tsx b/frontend/src/pages/TraceDetailV2/index.tsx
new file mode 100644
index 00000000000..20277942334
--- /dev/null
+++ b/frontend/src/pages/TraceDetailV2/index.tsx
@@ -0,0 +1,67 @@
+import './TraceDetailV2.styles.scss';
+
+import { Button, Tabs } from 'antd';
+import ROUTES from 'constants/routes';
+import history from 'lib/history';
+import { Compass, TowerControl, Undo } from 'lucide-react';
+import TraceDetail from 'pages/TraceDetail';
+import { useCallback, useState } from 'react';
+
+import TraceDetailsV2 from './TraceDetailV2';
+
+export default function TraceDetailsPage(): JSX.Element {
+ const [showOldTraceDetails, setShowOldTraceDetails] = useState(false);
+ const items = [
+ {
+ label: (
+
+ Explorer
+
+ ),
+ key: 'trace-details',
+ children: ,
+ },
+ {
+ label: (
+
+ Views
+
+ ),
+ key: 'saved-views',
+ children:
,
+ },
+ ];
+ const handleOldTraceDetails = useCallback(() => {
+ setShowOldTraceDetails(true);
+ }, []);
+
+ return showOldTraceDetails ? (
+
+ ) : (
+
+ {
+ if (activeKey === 'saved-views') {
+ history.push(ROUTES.TRACES_SAVE_VIEWS);
+ }
+ if (activeKey === 'trace-details') {
+ history.push(ROUTES.TRACES_EXPLORER);
+ }
+ }}
+ tabBarExtraContent={
+ }
+ >
+ Old Trace Details
+
+ }
+ />
+
+ );
+}
diff --git a/frontend/src/types/api/logs/log.ts b/frontend/src/types/api/logs/log.ts
index 1224f55fee7..d40dbb3d2e5 100644
--- a/frontend/src/types/api/logs/log.ts
+++ b/frontend/src/types/api/logs/log.ts
@@ -3,7 +3,7 @@ export interface ILog {
timestamp: number | string;
id: string;
traceId: string;
- spanId: string;
+ spanID: string;
traceFlags: number;
severityText: string;
severityNumber: number;
diff --git a/frontend/src/types/api/trace/getTraceFlamegraph.ts b/frontend/src/types/api/trace/getTraceFlamegraph.ts
new file mode 100644
index 00000000000..df199dd4e51
--- /dev/null
+++ b/frontend/src/types/api/trace/getTraceFlamegraph.ts
@@ -0,0 +1,26 @@
+export interface TraceDetailFlamegraphURLProps {
+ id: string;
+}
+
+export interface GetTraceFlamegraphPayloadProps {
+ traceId: string;
+ selectedSpanId: string;
+}
+
+export interface FlamegraphSpan {
+ timestamp: number;
+ durationNano: number;
+ spanId: string;
+ parentSpanId: string;
+ traceId: string;
+ hasError: boolean;
+ serviceName: string;
+ name: string;
+ level: number;
+}
+
+export interface GetTraceFlamegraphSuccessResponse {
+ spans: FlamegraphSpan[][];
+ startTimestampMillis: number;
+ endTimestampMillis: number;
+}
diff --git a/frontend/src/types/api/trace/getTraceV2.ts b/frontend/src/types/api/trace/getTraceV2.ts
new file mode 100644
index 00000000000..e11a6628514
--- /dev/null
+++ b/frontend/src/types/api/trace/getTraceV2.ts
@@ -0,0 +1,52 @@
+export interface TraceDetailV2URLProps {
+ id: string;
+}
+
+export interface GetTraceV2PayloadProps {
+ traceId: string;
+ selectedSpanId: string;
+ uncollapsedSpans: string[];
+ isSelectedSpanIDUnCollapsed: boolean;
+}
+
+export interface Event {
+ name: string;
+ timeUnixNano: number;
+ attributeMap: Record;
+}
+export interface Span {
+ timestamp: number;
+ durationNano: number;
+ spanId: string;
+ rootSpanId: string;
+ parentSpanId: string;
+ traceId: string;
+ hasError: boolean;
+ kind: number;
+ serviceName: string;
+ name: string;
+ references: any;
+ tagMap: Record;
+ event: string[];
+ rootName: string;
+ statusMessage: string;
+ statusCodeString: string;
+ spanKind: string;
+ hasChildren: boolean;
+ hasSibling: boolean;
+ subTreeNodeCount: number;
+ level: number;
+}
+
+export interface GetTraceV2SuccessResponse {
+ spans: Span[];
+ hasMissingSpans: boolean;
+ uncollapsedSpans: string[];
+ startTimestampMillis: number;
+ endTimestampMillis: number;
+ totalSpansCount: number;
+ totalErrorSpansCount: number;
+ rootServiceName: string;
+ rootServiceEntryPoint: string;
+ serviceNameToTotalDurationMap: Record;
+}
diff --git a/frontend/src/utils/timeUtils.ts b/frontend/src/utils/timeUtils.ts
index e93a96b4d25..3eb863363ed 100644
--- a/frontend/src/utils/timeUtils.ts
+++ b/frontend/src/utils/timeUtils.ts
@@ -28,6 +28,16 @@ export const getFormattedDateWithMinutes = (epochTimestamp: number): string => {
return date.format(DATE_TIME_FORMATS.MONTH_DATETIME_SHORT);
};
+export const getFormattedDateWithMinutesAndSeconds = (
+ epochTimestamp: number,
+): string => {
+ // Convert epoch timestamp to a date
+ const date = dayjs.unix(epochTimestamp);
+
+ // Format the date as "18 Nov 2013"
+ return date.format('DD MMM YYYY HH:mm:ss');
+};
+
export const getRemainingDays = (billingEndDate: number): number => {
// Convert Epoch timestamps to Date objects
const startDate = new Date(); // Convert seconds to milliseconds
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index bc745e556d3..bfa1f03cd7d 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -3690,11 +3690,23 @@
dependencies:
"@tanstack/table-core" "8.20.5"
+"@tanstack/react-virtual@3.11.2":
+ version "3.11.2"
+ resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz#d6b9bd999c181f0a2edce270c87a2febead04322"
+ integrity sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==
+ dependencies:
+ "@tanstack/virtual-core" "3.11.2"
+
"@tanstack/table-core@8.20.5":
version "8.20.5"
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d"
integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==
+"@tanstack/virtual-core@3.11.2":
+ version "3.11.2"
+ resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212"
+ integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==
+
"@testing-library/dom@^8.5.0":
version "8.20.0"
resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz"
diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go
index 89f8c0536de..ca0d1aaf2bf 100644
--- a/pkg/query-service/app/clickhouseReader/reader.go
+++ b/pkg/query-service/app/clickhouseReader/reader.go
@@ -1811,7 +1811,7 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, trace
}
processingPostCache := time.Now()
- selectedSpansForRequest := tracedetail.GetSelectedSpansForFlamegraphForRequest(req.SelectedSpanID, selectedSpans)
+ 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
diff --git a/pkg/query-service/app/traces/tracedetail/flamegraph.go b/pkg/query-service/app/traces/tracedetail/flamegraph.go
index b9412ebf8ce..b2c2c81b52b 100644
--- a/pkg/query-service/app/traces/tracedetail/flamegraph.go
+++ b/pkg/query-service/app/traces/tracedetail/flamegraph.go
@@ -8,6 +8,8 @@ import (
var (
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 {
@@ -39,9 +41,11 @@ func FindIndexForSelectedSpan(spans [][]*model.FlamegraphSpan, selectedSpanId st
var selectedSpanLevel int = 0
for index, _spans := range spans {
- if len(_spans) > 0 && _spans[0].SpanID == selectedSpanId {
- selectedSpanLevel = index
- break
+ for _, span := range _spans {
+ if span.SpanID == selectedSpanId {
+ selectedSpanLevel = index
+ break
+ }
}
}
@@ -88,7 +92,64 @@ func GetSelectedSpansForFlamegraph(traceRoots []*model.FlamegraphSpan, spanIdToS
return selectedSpans
}
-func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpans [][]*model.FlamegraphSpan) [][]*model.FlamegraphSpan {
+func getLatencyAndTimestampBucketedSpans(spans []*model.FlamegraphSpan, selectedSpanID string, isSelectedSpanIDPresent bool, startTime uint64, endTime uint64) []*model.FlamegraphSpan {
+ var sampledSpans []*model.FlamegraphSpan
+ // sort the spans by latency for latency filtering
+ sort.Slice(spans, func(i, j int) bool {
+ return spans[i].DurationNano > spans[j].DurationNano
+ })
+
+ // pick the top 5 latency spans
+ for idx := range 5 {
+ sampledSpans = append(sampledSpans, spans[idx])
+ }
+
+ // always add the selectedSpan
+ if isSelectedSpanIDPresent {
+ idx := -1
+ for _idx, span := range spans {
+ if span.SpanID == selectedSpanID {
+ idx = _idx
+ }
+ }
+ if idx != -1 {
+ sampledSpans = append(sampledSpans, spans[idx])
+ }
+ }
+
+ bucketSize := (endTime - startTime) / uint64(TIMESTAMP_SAMPLING_BUCKET_COUNT)
+ if bucketSize == 0 {
+ bucketSize = 1
+ }
+
+ 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 < 50 {
+ bucketedSpans[bucketIndex] = append(bucketedSpans[bucketIndex], span)
+ }
+ }
+ }
+
+ for i := range bucketedSpans {
+ if len(bucketedSpans[i]) > 2 {
+ // Keep only the first 2 spans
+ bucketedSpans[i] = bucketedSpans[i][:2]
+ }
+ }
+
+ // Flatten the bucketed spans into a single slice
+ for _, bucket := range bucketedSpans {
+ sampledSpans = append(sampledSpans, bucket...)
+ }
+
+ return sampledSpans
+}
+
+func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpans [][]*model.FlamegraphSpan, startTime uint64, endTime uint64) [][]*model.FlamegraphSpan {
+ var selectedSpansForRequest [][]*model.FlamegraphSpan
var selectedIndex = 0
if selectedSpanID != "" {
@@ -112,5 +173,14 @@ func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpan
lowerLimit = 0
}
- return selectedSpans[lowerLimit:upperLimit]
+ for i := lowerLimit; i < upperLimit; i++ {
+ if len(selectedSpans[i]) > SPAN_LIMIT_PER_LEVEL {
+ _spans := getLatencyAndTimestampBucketedSpans(selectedSpans[i], selectedSpanID, i == selectedIndex, startTime, endTime)
+ selectedSpansForRequest = append(selectedSpansForRequest, _spans)
+ } else {
+ selectedSpansForRequest = append(selectedSpansForRequest, selectedSpans[i])
+ }
+ }
+
+ return selectedSpansForRequest
}