Compare commits

..

3 Commits

Author SHA1 Message Date
nityanandagohain
18fa83ebfa fix: data race while getting cached series 2025-07-01 20:19:22 +05:30
Shaheer Kochai
9daefeb881 fix: override the stagedQuery orderBy and send order by timestamp in traces view of traces explorer (#8390)
* fix: override the stagedQuery orderBy and send order by timestamp in traces view of traces explorer

* chore: write test for sending order by timestamp in the traces view of traces explorer

* refactor: refactor the query transformer to accept partial query object and override fields
2025-07-01 14:14:07 +00:00
Shaheer Kochai
526cf01cb7 fix: fix the issue of traces filters getting duplicated on switching between the span scopes (#8389)
* fix: fix the issue of changing span scope duplicating filters

* chore: write test for duplicate filters issue on changing the span scope
2025-07-01 07:53:33 +00:00
9 changed files with 202 additions and 18 deletions

View File

@@ -98,6 +98,7 @@ interface QueryBuilderSearchV2Props {
hideSpanScopeSelector?: boolean;
// Determines whether to call onChange when a tag is closed
triggerOnChangeOnClose?: boolean;
skipQueryBuilderRedirect?: boolean;
}
export interface Option {
@@ -137,6 +138,7 @@ function QueryBuilderSearchV2(
operatorConfigKey,
hideSpanScopeSelector,
triggerOnChangeOnClose,
skipQueryBuilderRedirect,
} = props;
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
@@ -1038,7 +1040,11 @@ function QueryBuilderSearchV2(
})}
</Select>
{!hideSpanScopeSelector && (
<SpanScopeSelector query={query} onChange={onChange} />
<SpanScopeSelector
query={query}
onChange={onChange}
skipQueryBuilderRedirect={skipQueryBuilderRedirect}
/>
)}
</div>
);
@@ -1056,6 +1062,7 @@ QueryBuilderSearchV2.defaultProps = {
operatorConfigKey: undefined,
hideSpanScopeSelector: true,
triggerOnChangeOnClose: false,
skipQueryBuilderRedirect: false,
};
export default QueryBuilderSearchV2;

View File

@@ -23,6 +23,7 @@ interface SpanFilterConfig {
interface SpanScopeSelectorProps {
onChange?: (value: TagFilter) => void;
query?: IBuilderQuery;
skipQueryBuilderRedirect?: boolean;
}
const SPAN_FILTER_CONFIG: Record<SpanScope, SpanFilterConfig | null> = {
@@ -58,6 +59,7 @@ const SELECT_OPTIONS = [
function SpanScopeSelector({
onChange,
query,
skipQueryBuilderRedirect,
}: SpanScopeSelectorProps): JSX.Element {
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const [selectedScope, setSelectedScope] = useState<SpanScope>(
@@ -79,6 +81,7 @@ function SpanScopeSelector({
if (hasFilter('isEntryPoint')) return SpanScope.ENTRYPOINT_SPANS;
return SpanScope.ALL_SPANS;
};
useEffect(() => {
let queryData = (currentQuery?.builder?.queryData || [])?.find(
(item) => item.queryName === query?.queryName,
@@ -127,13 +130,10 @@ function SpanScopeSelector({
},
}));
if (onChange && query) {
if (skipQueryBuilderRedirect && onChange && query) {
onChange({
...query.filters,
items: getUpdatedFilters(
[...query.filters.items, ...newQuery.builder.queryData[0].filters.items],
true,
),
items: getUpdatedFilters([...query.filters.items], true),
});
setSelectedScope(newScope);
@@ -156,6 +156,7 @@ function SpanScopeSelector({
SpanScopeSelector.defaultProps = {
onChange: undefined,
query: undefined,
skipQueryBuilderRedirect: false,
};
export default SpanScopeSelector;

View File

@@ -3,9 +3,11 @@ import {
render,
RenderResult,
screen,
within,
} from '@testing-library/react';
import { initialQueriesMap } from 'constants/queryBuilder';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import { QueryClient, QueryClientProvider } from 'react-query';
import {
IBuilderQuery,
Query,
@@ -13,6 +15,7 @@ import {
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import QueryBuilderSearchV2 from '../QueryBuilderSearchV2';
import SpanScopeSelector from '../SpanScopeSelector';
const mockRedirectWithQueryBuilderData = jest.fn();
@@ -48,6 +51,14 @@ const defaultQuery = {
},
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
const defaultQueryBuilderQuery: IBuilderQuery = {
...initialQueriesMap.traces.builder.queryData[0],
queryName: 'A',
@@ -76,6 +87,7 @@ const renderWithContext = (
initialQuery = defaultQuery,
onChangeProp?: (value: TagFilter) => void,
queryProp?: IBuilderQuery,
skipQueryBuilderRedirect = false,
): RenderResult =>
render(
<QueryBuilderContext.Provider
@@ -87,12 +99,19 @@ const renderWithContext = (
} as any
}
>
<SpanScopeSelector onChange={onChangeProp} query={queryProp} />
<SpanScopeSelector
onChange={onChangeProp}
query={queryProp}
skipQueryBuilderRedirect={skipQueryBuilderRedirect}
/>
</QueryBuilderContext.Provider>,
);
const selectOption = async (optionText: string): Promise<void> => {
const selector = screen.getByRole('combobox');
const selector = within(screen.getByTestId('span-scope-selector')).getByRole(
'combobox',
);
fireEvent.mouseDown(selector);
// Wait for dropdown to appear
@@ -264,6 +283,7 @@ describe('SpanScopeSelector', () => {
defaultQuery,
mockOnChange,
localQuery,
true,
);
expect(await screen.findByText('All Spans')).toBeInTheDocument();
@@ -283,6 +303,7 @@ describe('SpanScopeSelector', () => {
defaultQuery,
mockOnChange,
localQuery,
true,
);
expect(await screen.findByText('Root Spans')).toBeInTheDocument();
@@ -303,6 +324,7 @@ describe('SpanScopeSelector', () => {
defaultQuery,
mockOnChange,
localQuery,
true,
);
expect(await screen.findByText('Root Spans')).toBeInTheDocument();
@@ -324,6 +346,7 @@ describe('SpanScopeSelector', () => {
defaultQuery,
mockOnChange,
localQuery,
true,
);
expect(await screen.findByText('Root Spans')).toBeInTheDocument();
@@ -350,6 +373,7 @@ describe('SpanScopeSelector', () => {
defaultQuery,
mockOnChange,
localQuery,
true,
);
expect(await screen.findByText('Entrypoint Spans')).toBeInTheDocument();
@@ -361,5 +385,60 @@ describe('SpanScopeSelector', () => {
container.querySelector('span[title="All Spans"]'),
).toBeInTheDocument();
});
it('should not duplicate non-scope filters when changing span scope', async () => {
const query = {
...defaultQuery,
builder: {
...defaultQuery.builder,
queryData: [
{
...defaultQuery.builder.queryData[0],
filters: {
items: [createNonScopeFilter('service', 'checkout')],
op: 'AND',
},
},
],
},
};
render(
<QueryClientProvider client={queryClient}>
<QueryBuilderContext.Provider
value={
{
currentQuery: query,
redirectWithQueryBuilderData: mockRedirectWithQueryBuilderData,
} as any
}
>
<QueryBuilderSearchV2
query={query.builder.queryData[0] as any}
onChange={mockOnChange}
hideSpanScopeSelector={false}
/>
</QueryBuilderContext.Provider>
</QueryClientProvider>,
);
expect(await screen.findByText('All Spans')).toBeInTheDocument();
await selectOption('Entrypoint Spans');
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalled();
const redirectQueryArg = mockRedirectWithQueryBuilderData.mock
.calls[0][0] as Query;
const { items } = redirectQueryArg.builder.queryData[0].filters;
// Count non-scope filters
const nonScopeFilters = items.filter(
(filter) => filter.key?.type !== 'spanSearchScope',
);
expect(nonScopeFilters).toHaveLength(1);
expect(nonScopeFilters).toContainEqual(
createNonScopeFilter('service', 'checkout'),
);
});
});
});

View File

@@ -142,6 +142,7 @@ function Filters({
}}
onChange={handleFilterChange}
hideSpanScopeSelector={false}
skipQueryBuilderRedirect
/>
{filteredSpanIds.length > 0 && (
<div className="pre-next-toggle">

View File

@@ -17,6 +17,7 @@ import { AppState } from 'store/reducers';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import DOCLINKS from 'utils/docLinks';
import { transformBuilderQueryFields } from 'utils/queryTransformers';
import TraceExplorerControls from '../Controls';
import { TracesLoading } from '../TraceLoading/TraceLoading';
@@ -39,9 +40,22 @@ function TracesView({ isFilterApplied }: TracesViewProps): JSX.Element {
QueryParams.pagination,
);
const transformedQuery = useMemo(
() =>
transformBuilderQueryFields(stagedQuery || initialQueriesMap.traces, {
orderBy: [
{
columnName: 'timestamp',
order: 'desc',
},
],
}),
[stagedQuery],
);
const { data, isLoading, isFetching, isError } = useGetQueryRange(
{
query: stagedQuery || initialQueriesMap.traces,
query: transformedQuery,
graphType: panelType || PANEL_TYPES.TRACE,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime,

View File

@@ -594,6 +594,53 @@ describe('TracesExplorer - ', () => {
'http://localhost/trace/5765b60ba7cc4ddafe8bdaa9c1b4b246',
);
});
it('trace explorer - trace view should only send order by timestamp in the query', async () => {
let capturedPayload: QueryRangePayload;
const orderBy = [
{ columnName: 'id', order: 'desc' },
{ columnName: 'serviceName', order: 'desc' },
];
const defaultOrderBy = [{ columnName: 'timestamp', order: 'desc' }];
server.use(
rest.post(`${BASE_URL}/api/v4/query_range`, async (req, res, ctx) => {
const payload = await req.json();
capturedPayload = payload;
return res(ctx.status(200), ctx.json(queryRangeForTraceView));
}),
);
render(
<QueryBuilderContext.Provider
value={{
...qbProviderValue,
panelType: PANEL_TYPES.TRACE,
stagedQuery: {
...qbProviderValue.stagedQuery,
builder: {
...qbProviderValue.stagedQuery.builder,
queryData: [
{
...qbProviderValue.stagedQuery.builder.queryData[0],
orderBy,
},
],
},
},
}}
>
<TracesExplorer />
</QueryBuilderContext.Provider>,
);
await waitFor(() => {
expect(capturedPayload).toBeDefined();
expect(capturedPayload?.compositeQuery?.builderQueries?.A.orderBy).toEqual(
defaultOrderBy,
);
expect(
capturedPayload?.compositeQuery?.builderQueries?.A.orderBy,
).not.toEqual(orderBy);
});
});
it('test for explorer options', async () => {
const { getByText, getByTestId } = render(

View File

@@ -0,0 +1,28 @@
import { cloneDeep } from 'lodash-es';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
/**
* Transforms a query by modifying specific fields in the builder queries
* @param query - The original query object
* @param fieldOverrides - Partial object containing fields to override in each builder query
* @returns A new query object with the modified fields
*/
export const transformBuilderQueryFields = (
query: Query,
fieldOverrides: Partial<IBuilderQuery>,
): Query => {
// Create a deep copy of the query
const transformedQuery: Query = cloneDeep(query);
// Update the specified fields for each query in the builder
if (transformedQuery.builder?.queryData) {
transformedQuery.builder.queryData = transformedQuery.builder.queryData.map(
(queryItem) => ({
...queryItem,
...fieldOverrides,
}),
);
}
return transformedQuery;
};

View File

@@ -236,7 +236,14 @@ func (q *queryCache) FindMissingTimeRanges(orgID valuer.UUID, start, end, step i
func (q *queryCache) getCachedSeriesData(orgID valuer.UUID, cacheKey string) []*CachedSeriesData {
cacheableSeriesData := new(CacheableSeriesData)
err := q.cache.Get(context.TODO(), orgID, cacheKey, cacheableSeriesData, true)
tmpcacheableSeriesData := new(CacheableSeriesData)
err := q.cache.Get(context.TODO(), orgID, cacheKey, tmpcacheableSeriesData, true)
data, err := tmpcacheableSeriesData.MarshalBinary()
if err != nil {
zap.L().Error("error marshalling cacheable series data", zap.Error(err))
}
cacheableSeriesData.UnmarshalBinary(data)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil
}
@@ -300,11 +307,18 @@ func (q *queryCache) MergeWithCachedSeriesDataV2(orgID valuer.UUID, cacheKey str
return newData
}
tmpcacheableSeriesData := new(CacheableSeriesData)
cacheableSeriesData := new(CacheableSeriesData)
err := q.cache.Get(context.TODO(), orgID, cacheKey, cacheableSeriesData, true)
err := q.cache.Get(context.TODO(), orgID, cacheKey, tmpcacheableSeriesData, true)
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
return nil
}
data, err := tmpcacheableSeriesData.MarshalBinary()
if err != nil {
zap.L().Error("error marshalling cacheable series data", zap.Error(err))
}
cacheableSeriesData.UnmarshalBinary(data)
allData := append(cacheableSeriesData.Series, newData...)
sort.Slice(allData, func(i, j int) bool {

View File

@@ -353,10 +353,6 @@ func (m *Manager) EditRule(ctx context.Context, ruleStr string, id valuer.UUID)
return err
}
if len(channels) == 0 {
return errors.New("no channels found for this org, please set channels first")
}
for _, channel := range channels {
preferredChannels = append(preferredChannels, channel.Name)
}
@@ -540,9 +536,6 @@ func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*ruletypes.Ge
if err != nil {
return err
}
if len(channels) == 0 {
return errors.New("no channels found for this org, please set channels first")
}
for _, channel := range channels {
preferredChannels = append(preferredChannels, channel.Name)