Compare commits

..

1 Commits

Author SHA1 Message Date
Vinícius Lourenço
72eb26419a fix(sentry): drop errors thrown by PostHog, Pylon and Appcues scripts 2026-10-28 18:27:41 -03:00
36 changed files with 398 additions and 325 deletions

View File

@@ -0,0 +1,88 @@
import type { ErrorEvent } from '@sentry/react';
import {
isExternalIntegrationError,
SENTRY_IGNORE_ERRORS,
} from '../sentryFilters';
const APP_BUNDLE = 'https://example.us.signoz.cloud/assets/index-B3QqK3ie.js';
function eventWithFrames(...filenames: string[]): ErrorEvent {
return {
type: undefined,
exception: {
values: [
{
type: 'TypeError',
value: 'Failed to fetch',
stacktrace: {
frames: filenames.map((filename) => ({ filename, lineno: 1, colno: 1 })),
},
},
],
},
};
}
describe('isExternalIntegrationError', () => {
it.each([
[
'PostHog recorder',
'https://us-assets.i.posthog.com/static/posthog-recorder.js?v=1.298.0',
],
[
'Pylon widget',
'https://widget.usepylon.com/widget/00000000-0000-0000-0000-000000000000',
],
['Appcues', 'https://fast.appcues.com/123456.js'],
])('drops an error with a %s frame', (_, url) => {
expect(isExternalIntegrationError(eventWithFrames(APP_BUNDLE, url))).toBe(
true,
);
});
it('drops it when the top frame is our bundle', () => {
expect(
isExternalIntegrationError(
eventWithFrames(
'https://us-assets.i.posthog.com/static/posthog-recorder.js?v=1.298.0',
'<anonymous>',
APP_BUNDLE,
),
),
).toBe(true);
});
it('keeps an error from our bundle only', () => {
expect(
isExternalIntegrationError(eventWithFrames(APP_BUNDLE, '<anonymous>')),
).toBe(false);
});
it('keeps an error from a host that only contains an integration name', () => {
expect(
isExternalIntegrationError(
eventWithFrames(
'https://posthog.example.com/app.js',
'https://notusepylon.com/widget.js',
),
),
).toBe(false);
});
it('keeps an error without a stack trace', () => {
expect(isExternalIntegrationError({ type: undefined })).toBe(false);
});
});
describe('SENTRY_IGNORE_ERRORS', () => {
it('ignores the bundled PostHog recorder load failure', () => {
expect(
SENTRY_IGNORE_ERRORS.some((pattern) =>
pattern.test(
'Error: Called on script loaded before session recording is available',
),
),
).toBe(true);
});
});

View File

@@ -37,6 +37,10 @@ import defaultRoutes, {
LIST_LICENSES,
SUPPORT_ROUTE,
} from './routes';
import {
isExternalIntegrationError,
SENTRY_IGNORE_ERRORS,
} from './sentryFilters';
const appRouter = (children: ReactNode): ReactNode => (
<Router history={history}>
@@ -310,16 +314,7 @@ function App(): JSX.Element {
tunnel: window.signozBootData.settings.sentry.tunnel,
environment: process.env.ENVIRONMENT,
release: process.env.VERSION,
// A tab that outlived a deploy requests hashed assets the new build no longer
// has. `lazyRetry` recovers by reloading once, so this class is not worth
// reporting. The stylesheet message is Vite's own; the module ones are the
// same failure worded differently by Chromium, Firefox and Safari.
ignoreErrors: [
/Unable to preload CSS for/,
/Failed to fetch dynamically imported module/,
/error loading dynamically imported module/,
/Importing a module script failed/,
],
ignoreErrors: SENTRY_IGNORE_ERRORS,
integrations: [
// Kept for the `transaction` tag used in routing, even though
// tracing is disabled. Ref: https://github.com/SigNoz/platform-pod/issues/2393#issuecomment-4603658055
@@ -354,6 +349,10 @@ function App(): JSX.Element {
return null;
}
if (isExternalIntegrationError(event)) {
return null;
}
const sessionReplayUrl = posthog.get_session_replay_url?.({
withTimestamp: true,
});

View File

@@ -0,0 +1,32 @@
import type { ErrorEvent } from '@sentry/react';
export const SENTRY_IGNORE_ERRORS = [
// A tab that outlived a deploy requests hashed assets the new build no longer
// has. `lazyRetry` recovers by reloading once, so this class is not worth
// reporting. The stylesheet message is Vite's own; the module ones are the
// same failure worded differently by Chromium, Firefox and Safari.
/Unable to preload CSS for/,
/Failed to fetch dynamically imported module/,
/error loading dynamically imported module/,
/Importing a module script failed/,
// Thrown by the bundled posthog-js when its recorder script fails to load, so
// its frames point at our own bundle and the URL check below misses it.
/Called on script loaded before session recording is available/,
];
// PostHog's lazy recorder, the Pylon widget and Appcues.
const EXTERNAL_SCRIPT_URLS = [
/^https?:\/\/[^/]+\.posthog\.com\//,
/^https?:\/\/(?:[^/]+\.)?usepylon\.com\//,
/^https?:\/\/(?:[^/]+\.)?appcues\.(?:com|net)\//,
];
// Checks every frame, not only the top one: a failed fetch from the recorder
// tops out in Sentry's own fetch wrapper, which is bundled with the app.
export function isExternalIntegrationError(event: ErrorEvent): boolean {
return (event.exception?.values ?? []).some((exception) =>
(exception.stacktrace?.frames ?? []).some((frame) =>
EXTERNAL_SCRIPT_URLS.some((url) => url.test(frame.filename ?? '')),
),
);
}

View File

@@ -4,7 +4,6 @@ import { Button } from '@signozhq/ui/button';
import { Drawer, Tooltip } from 'antd';
import { ToggleGroup } from '@signozhq/ui/toggle-group';
import { Typography } from '@signozhq/ui/typography';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import LogStateIndicator from 'components/Logs/LogStateIndicator/LogStateIndicator';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
@@ -148,7 +147,7 @@ function LogDetailInner({
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});

View File

@@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import FieldsSelector from 'components/FieldsSelector';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
@@ -55,7 +54,7 @@ function LiveLogsContainer({
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});

View File

@@ -5,7 +5,6 @@ import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { toast } from '@signozhq/ui/sonner';
import { Card } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import ListLogView from 'components/Logs/ListLogView';
@@ -31,7 +30,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { useEventSource } from 'providers/EventSource';
// interfaces
import { ILog } from 'types/api/logs/log';
import { StringOperators } from 'types/common/queryBuilder';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import loadingPlaneUrl from '@/assets/Icons/loading-plane.gif';
import { getAbsoluteUrl } from 'utils/basePath';
@@ -69,7 +68,7 @@ function LiveLogsList({
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator: StringOperators.NOOP,
});

View File

@@ -1,7 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Virtuoso } from 'react-virtuoso';
import { Button, Skeleton } from 'antd';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import RawLogView from 'components/Logs/RawLogView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { LOCALSTORAGE } from 'constants/localStorage';
@@ -17,7 +16,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useUrlQuery from 'hooks/useUrlQuery';
import { ILog } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { StringOperators } from 'types/common/queryBuilder';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { withBasePath } from 'utils/basePath';
import { useContextLogData } from './useContextLogData';
@@ -46,7 +45,7 @@ function ContextLogRenderer({
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});

View File

@@ -5,7 +5,6 @@ import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { toast } from '@signozhq/ui/sonner';
import { Card } from 'antd';
import logEvent from 'api/common/logEvent';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
@@ -78,7 +77,7 @@ function LogsExplorerList({
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator:
currentStagedQueryData?.aggregateOperator || StringOperators.NOOP,
});

View File

@@ -1,7 +1,6 @@
import { ReactNode, useState } from 'react';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
import FieldsSelector from 'components/FieldsSelector';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
@@ -29,7 +28,7 @@ function LogsActionsContainer({
}): JSX.Element {
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});

View File

@@ -4,7 +4,7 @@ import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKey
import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { DataSource } from 'types/common/queryBuilder';
import useOptionsMenu from '../useOptionsMenu';
@@ -32,7 +32,7 @@ describe('useOptionsMenu', () => {
});
(usePreferenceContext as jest.Mock).mockReturnValue({
[SavedviewtypesSourceDTO.traces]: {
traces: {
preferences: {
columns: [],
formatting: {
@@ -44,7 +44,7 @@ describe('useOptionsMenu', () => {
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
[SavedviewtypesSourceDTO.logs]: {
logs: {
preferences: {
columns: [],
formatting: {
@@ -66,7 +66,7 @@ describe('useOptionsMenu', () => {
(useQueries as jest.Mock).mockReturnValue([]);
});
it('does not show isRoot or isEntryPoint in column options when sourcePage is traces', () => {
it('does not show isRoot or isEntryPoint in column options when dataSource is TRACES', () => {
// Mock the query key suggestions to return data including isRoot and isEntryPoint
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: {
@@ -108,7 +108,7 @@ describe('useOptionsMenu', () => {
const { result } = renderHook(() =>
useOptionsMenu({
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
aggregateOperator: 'count',
}),
);
@@ -127,7 +127,60 @@ describe('useOptionsMenu', () => {
expect(optionNames).toContain('serviceName');
});
it('shows body in column options when sourcePage is logs', () => {
it('does not show body in column options when dataSource is METRICS', () => {
// Mock the query key suggestions to return data including body
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: {
data: {
data: {
keys: {
attributeKeys: [
{
name: 'body',
signal: 'logs',
fieldDataType: 'string',
fieldContext: '',
},
{
name: 'status',
signal: 'metrics',
fieldDataType: 'int64',
fieldContext: '',
},
{
name: 'value',
signal: 'metrics',
fieldDataType: 'float64',
fieldContext: '',
},
],
},
},
},
},
isFetching: false,
});
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.METRICS,
aggregateOperator: 'count',
}),
);
// Get the column options from the config
const columnOptions = result.current.config.addColumn?.options ?? [];
const optionNames = columnOptions.map((option) => option.label);
// body should NOT be in the options
expect(optionNames).not.toContain('body');
// Other attributes should be present
expect(optionNames).toContain('status');
expect(optionNames).toContain('value');
});
it('does not show body in column options when dataSource is LOGS', () => {
// Mock the query key suggestions to return data including body
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: {
@@ -163,7 +216,7 @@ describe('useOptionsMenu', () => {
const { result } = renderHook(() =>
useOptionsMenu({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator: 'count',
}),
);
@@ -222,12 +275,12 @@ describe('useOptionsMenu', () => {
isFetching: false,
});
(usePreferenceContext as jest.Mock).mockReturnValue({
[SavedviewtypesSourceDTO.traces]: {
traces: {
preferences: { columns: [], formatting: baseFormatting },
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
[SavedviewtypesSourceDTO.logs]: {
logs: {
preferences: { columns: seedColumns, formatting: baseFormatting },
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
@@ -238,7 +291,7 @@ describe('useOptionsMenu', () => {
it('reorders by composite IDs — preserves both same-name variants distinctly', () => {
const { result } = renderHook(() =>
useOptionsMenu({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator: 'count',
}),
);
@@ -269,7 +322,7 @@ describe('useOptionsMenu', () => {
it('reorder ignores ids that are not columns (e.g. state-indicator) and skips unknown ids', () => {
const { result } = renderHook(() =>
useOptionsMenu({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator: 'count',
}),
);
@@ -301,7 +354,7 @@ describe('useOptionsMenu', () => {
it('removes only the variant whose composite ID matches', () => {
const { result } = renderHook(() =>
useOptionsMenu({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator: 'count',
}),
);
@@ -322,7 +375,7 @@ describe('useOptionsMenu', () => {
it('removing by a non-matching composite ID is a no-op (filter returns the full list)', () => {
const { result } = renderHook(() =>
useOptionsMenu({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
aggregateOperator: 'count',
}),
);
@@ -342,7 +395,7 @@ describe('useOptionsMenu', () => {
isFetching: false,
});
(usePreferenceContext as jest.Mock).mockReturnValue({
[SavedviewtypesSourceDTO.traces]: {
traces: {
preferences: {
columns: [
{ name: 'body', fieldContext: 'log' },
@@ -354,7 +407,7 @@ describe('useOptionsMenu', () => {
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
[SavedviewtypesSourceDTO.logs]: {
logs: {
preferences: { columns: [], formatting: {} },
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
@@ -363,7 +416,7 @@ describe('useOptionsMenu', () => {
const { result } = renderHook(() =>
useOptionsMenu({
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
aggregateOperator: 'count',
}),
);

View File

@@ -1,6 +1,4 @@
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { TelemetryFieldKey } from 'api/v5/v5';
import { PREFERENCE_SOURCE_PAGES } from 'providers/preferences/constants';
import { DataSource } from 'types/common/queryBuilder';
import { FontSize, OptionsQuery } from './types';
@@ -15,14 +13,6 @@ export const defaultOptionsQuery: OptionsQuery = {
fontSize: FontSize.SMALL,
};
export const SOURCE_PAGE_SIGNAL: Record<
(typeof PREFERENCE_SOURCE_PAGES)[number],
DataSource
> = {
[SavedviewtypesSourceDTO.logs]: DataSource.LOGS,
[SavedviewtypesSourceDTO.traces]: DataSource.TRACES,
};
export const EXCLUDED_COLUMNS: Record<DataSource, string[]> = {
[DataSource.TRACES]: ['body', 'isRoot', 'isEntryPoint'],
[DataSource.METRICS]: ['body'],

View File

@@ -1,7 +1,6 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueries } from 'react-query';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { TelemetryFieldKey } from 'api/v5/v5';
import { AxiosResponse } from 'axios';
@@ -12,7 +11,6 @@ import useUrlQueryData from 'hooks/useUrlQueryData';
import { has } from 'lodash-es';
import { AllTraceFilterKeyValue } from 'constants/traceFilterKeys';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import { PreferenceSourcePage } from 'providers/preferences/types';
import {
QueryKeyRequestProps,
QueryKeySuggestionsResponseProps,
@@ -22,13 +20,13 @@ import {
FieldDataType,
SignalType,
} from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
import {
defaultLogsSelectedColumns,
defaultOptionsQuery,
defaultTraceSelectedColumns,
EXCLUDED_COLUMNS,
SOURCE_PAGE_SIGNAL,
URL_OPTIONS,
} from './constants';
import {
@@ -42,7 +40,7 @@ import { buildCompositeKey, getOptionsFromKeys } from './utils';
interface UseOptionsMenuProps {
storageKey?: string;
sourcePage: PreferenceSourcePage;
dataSource: DataSource;
aggregateOperator?: string;
initialOptions?: InitialOptions;
}
@@ -54,14 +52,15 @@ interface UseOptionsMenu {
}
const useOptionsMenu = ({
sourcePage,
dataSource,
// aggregateOperator,
initialOptions = {},
}: UseOptionsMenuProps): UseOptionsMenu => {
const signal = SOURCE_PAGE_SIGNAL[sourcePage];
const { notifications } = useNotifications();
const prefCtx = usePreferenceContext();
// TODO: send null to updateColumns and updateFormatting if dataSource is not logs or traces
const { preferences, updateColumns, updateFormatting } =
usePreferenceContext()[sourcePage];
const slice = dataSource === DataSource.TRACES ? prefCtx.traces : prefCtx.logs;
const { preferences, updateColumns, updateFormatting } = slice;
const [searchText, setSearchText] = useState<string>('');
const [isFocused, setIsFocused] = useState<boolean>(false);
@@ -69,10 +68,10 @@ const useOptionsMenu = ({
const initialQueryParamsV5: QueryKeyRequestProps = useMemo(
() => ({
signal,
signal: dataSource,
searchText: '',
}),
[signal],
[dataSource],
);
const { query: optionsQuery, redirectWithQuery: redirectWithOptionsData } =
@@ -128,7 +127,7 @@ const useOptionsMenu = ({
?.map((column) => attributesData.find(({ name }) => name === column))
.filter((e) => !!e) || []) as TelemetryFieldKey[];
if (sourcePage === SavedviewtypesSourceDTO.traces) {
if (dataSource === DataSource.TRACES) {
initialSelected = initialSelected
?.map((col) => {
if (col && Object.keys(AllTraceFilterKeyValue).includes(col?.name)) {
@@ -158,7 +157,7 @@ const useOptionsMenu = ({
isFetchedInitialAttributes,
initialOptions?.selectColumns,
initialAttributesResult,
sourcePage,
dataSource,
]);
const {
@@ -196,7 +195,7 @@ const useOptionsMenu = ({
fieldDataType: e.fieldDataType,
}));
}
if (sourcePage === SavedviewtypesSourceDTO.traces) {
if (dataSource === DataSource.TRACES) {
return defaultTraceSelectedColumns.map((e) => ({
...e,
name: e.name,
@@ -204,16 +203,16 @@ const useOptionsMenu = ({
}
return [];
}, [sourcePage, searchedAttributesDataV5?.data.data.keys]);
}, [dataSource, searchedAttributesDataV5?.data.data.keys]);
const initialOptionsQuery: OptionsQuery = useMemo(() => {
let defaultColumns: TelemetryFieldKey[] = defaultOptionsQuery.selectColumns;
if (sourcePage === SavedviewtypesSourceDTO.traces) {
if (dataSource === DataSource.TRACES) {
defaultColumns = defaultTraceSelectedColumns.map((e) => ({
...e,
name: e.name,
}));
} else if (sourcePage === SavedviewtypesSourceDTO.logs) {
} else if (dataSource === DataSource.LOGS) {
// eslint-disable-next-line sonarjs/no-identical-functions
defaultColumns = defaultLogsSelectedColumns.map((e) => ({
...e,
@@ -233,7 +232,7 @@ const useOptionsMenu = ({
...initialOptions,
selectColumns: finalSelectColumns,
};
}, [sourcePage, initialOptions, initialSelectedColumns]);
}, [dataSource, initialOptions, initialSelectedColumns]);
const selectedColumnKeys = useMemo(
() => preferences?.columns?.map(({ name }) => name) || [],
@@ -242,7 +241,7 @@ const useOptionsMenu = ({
const optionsFromAttributeKeys = useMemo(() => {
const filteredAttributeKeys = searchedAttributeKeys.filter((item) => {
const exclusions = EXCLUDED_COLUMNS[signal];
const exclusions = EXCLUDED_COLUMNS[dataSource];
if (exclusions) {
return !exclusions.includes(item.name);
}
@@ -250,7 +249,7 @@ const useOptionsMenu = ({
});
return getOptionsFromKeys(filteredAttributeKeys, selectedColumnKeys);
}, [signal, searchedAttributeKeys, selectedColumnKeys]);
}, [dataSource, searchedAttributeKeys, selectedColumnKeys]);
const handleRedirectWithOptionsData = useCallback(
(newQueryData: OptionsQuery) => {
@@ -286,10 +285,7 @@ const useOptionsMenu = ({
buildCompositeKey(f.name, f.fieldContext, f.fieldDataType) !== columnKey,
);
if (
!newSelectedColumns?.length &&
sourcePage !== SavedviewtypesSourceDTO.logs
) {
if (!newSelectedColumns?.length && dataSource !== DataSource.LOGS) {
notifications.error({
message: 'There must be at least one selected column',
});
@@ -298,7 +294,7 @@ const useOptionsMenu = ({
updateColumns(newSelectedColumns || []);
},
[sourcePage, notifications, preferences, updateColumns],
[dataSource, notifications, preferences, updateColumns],
);
const handleFormatChange = useCallback(

View File

@@ -28,7 +28,7 @@ const errors = {
};
function Harness(): JSX.Element {
const traces = usePreferenceContext()[SavedviewtypesSourceDTO.traces];
const { traces } = usePreferenceContext();
const [headerKey, setHeaderKey] = useState(0);
const columns = traces.preferences?.columns ?? [];

View File

@@ -1,16 +1,28 @@
import { Copy, Link, PenLine, Trash2 } from '@signozhq/icons';
import { SavedviewtypesSchemaVersionDTO } from 'api/generated/services/sigNoz.schemas';
import {
SavedviewtypesSchemaVersionDTO,
SavedviewtypesSourceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import {
buildSavedViewDeletePermission,
buildSavedViewUpdatePermission,
} from 'lib/authz/hooks/useAuthZ/permissions/saved-view.permissions';
import { DataSource } from 'types/common/queryBuilder';
import { SavedViewRowActionConfig, SaveViewModalMode } from './types';
export const SAVED_VIEW_SCHEMA_VERSION = SavedviewtypesSchemaVersionDTO.v2;
// Only the logs and traces explorers have columns and formatting to save.
export const SAVED_VIEW_OPTIONS_DATA_SOURCE: Partial<
Record<SavedviewtypesSourceDTO, DataSource>
> = {
[SavedviewtypesSourceDTO.logs]: DataSource.LOGS,
[SavedviewtypesSourceDTO.traces]: DataSource.TRACES,
};
// What selecting a view writes to the url, so clearing it removes exactly these.
export const SAVED_VIEW_URL_PARAMS = [
QueryParams.viewKey,

View File

@@ -8,9 +8,10 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
import { buildSavedViewReadPermission } from 'lib/authz/hooks/useAuthZ/permissions/saved-view.permissions';
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
import { isPreferenceSourcePage } from 'providers/preferences/utils';
import { DataSource } from 'types/common/queryBuilder';
import { toAPIError } from 'utils/errorUtils';
import { SAVED_VIEW_OPTIONS_DATA_SOURCE } from '../constants';
import { UseActiveSavedViewResult } from '../types';
import { hasUnsavedViewChanges } from '../utils/hasUnsavedViewChanges';
@@ -20,9 +21,9 @@ export function useActiveSavedView(
const { viewKey } = useGetSavedViewParams();
const { stagedQuery, panelType } = useQueryBuilder();
const hasOptions = isPreferenceSourcePage(source);
const optionsDataSource = SAVED_VIEW_OPTIONS_DATA_SOURCE[source];
const { options } = useOptionsMenu({
sourcePage: hasOptions ? source : SavedviewtypesSourceDTO.logs,
dataSource: optionsDataSource ?? DataSource.LOGS,
});
const { isLoading: isCheckingRead, deniedPermissions } = useAuthZ(
@@ -62,8 +63,7 @@ export function useActiveSavedView(
view,
stagedQuery,
panelType: panelType ?? PANEL_TYPES.LIST,
options: hasOptions ? options : undefined,
sourcePage: source,
options: optionsDataSource ? options : undefined,
});
}, [
view,
@@ -71,8 +71,7 @@ export function useActiveSavedView(
isStagedQueryCurrent,
stagedQuery,
panelType,
hasOptions,
source,
optionsDataSource,
options,
]);

View File

@@ -20,12 +20,13 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { usePreferenceSlice } from 'providers/preferences/context/usePreferenceSlice';
import { isPreferenceSourcePage } from 'providers/preferences/utils';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import { DataSource } from 'types/common/queryBuilder';
import { toAPIError } from 'utils/errorUtils';
import { getUnstableCurrentSearchParams } from 'utils/getUnstableCurrentSearchParams';
import {
SAVED_VIEW_OPTIONS_DATA_SOURCE,
SAVED_VIEW_SCHEMA_VERSION,
SAVED_VIEW_TOAST_POSITION,
SAVED_VIEW_URL_PARAMS,
@@ -59,11 +60,15 @@ export function useSavedViewActions(
const { viewKey } = useGetSavedViewParams();
const logSavedViewEvent = useSavedViewLogEvent(source);
const hasOptions = isPreferenceSourcePage(source);
const optionsDataSource = SAVED_VIEW_OPTIONS_DATA_SOURCE[source];
const { options } = useOptionsMenu({
sourcePage: hasOptions ? source : SavedviewtypesSourceDTO.logs,
dataSource: optionsDataSource ?? DataSource.LOGS,
});
const preferenceSlice = usePreferenceSlice(source);
const preferences = usePreferenceContext();
const preferencesSlice =
optionsDataSource === DataSource.TRACES
? preferences.traces
: preferences.logs;
const { mutateAsync: createSavedView, isLoading: isCreating } =
useCreateSavedView();
@@ -81,10 +86,10 @@ export function useSavedViewActions(
query: stagedQuery ?? currentQuery,
panelType: panelType ?? PANEL_TYPES.LIST,
displayName,
options: hasOptions ? options : undefined,
options: optionsDataSource ? options : undefined,
color,
}),
[stagedQuery, currentQuery, panelType, hasOptions, options],
[stagedQuery, currentQuery, panelType, optionsDataSource, options],
);
const selectView = useCallback(
@@ -100,14 +105,14 @@ export function useSavedViewActions(
const revertView = useCallback(
(view: SavedviewtypesSavedViewDTO): void => {
if (preferenceSlice) {
if (optionsDataSource) {
const { columns, formatting } = getViewColumnsAndFormatting(
view.spec,
source,
optionsDataSource,
);
preferenceSlice.updateColumns(columns);
preferencesSlice.updateColumns(columns);
if (formatting) {
preferenceSlice.updateFormatting(formatting);
preferencesSlice.updateFormatting(formatting);
}
}
selectView(view);
@@ -115,7 +120,7 @@ export function useSavedViewActions(
[SavedViewEventKeys.ViewId]: view.id,
});
},
[source, preferenceSlice, selectView, logSavedViewEvent],
[optionsDataSource, preferencesSlice, selectView, logSavedViewEvent],
);
// The query builder redirects only ever add params, so the url is rebuilt

View File

@@ -32,7 +32,6 @@ export interface HasUnsavedViewChangesArgs {
stagedQuery: Query | null;
panelType: PANEL_TYPES;
options?: SavedViewOptions;
sourcePage: SavedviewtypesSourceDTO;
}
export interface UseLastUsedViewResult {

View File

@@ -1,7 +1,6 @@
import {
SavedviewtypesSavedViewDTO,
SavedviewtypesSavedViewSpecDTO,
SavedviewtypesSourceDTO,
} from 'api/generated/services/sigNoz.schemas';
import {
initialQueriesMap,
@@ -121,7 +120,6 @@ describe('hasUnsavedViewChanges', () => {
view,
stagedQuery: staged,
panelType: PANEL_TYPES.LIST,
sourcePage: SavedviewtypesSourceDTO.traces,
options: { ...options, selectColumns: [columns[0]] },
}),
).toBe(true);
@@ -141,7 +139,6 @@ describe('hasUnsavedViewChanges', () => {
view: bare,
stagedQuery: stagedFrom(bare, DataSource.TRACES),
panelType: PANEL_TYPES.LIST,
sourcePage: SavedviewtypesSourceDTO.traces,
options: { ...options, selectColumns: defaultTraceSelectedColumns },
}),
).toBe(false);
@@ -166,7 +163,6 @@ describe('hasUnsavedViewChanges', () => {
view: logsView,
stagedQuery: logsStaged,
panelType: PANEL_TYPES.LIST,
sourcePage: SavedviewtypesSourceDTO.logs,
options: { selectColumns: shownColumns, ...formatting },
}),
).toBe(false);
@@ -178,7 +174,6 @@ describe('hasUnsavedViewChanges', () => {
view: logsView,
stagedQuery: logsStaged,
panelType: PANEL_TYPES.LIST,
sourcePage: SavedviewtypesSourceDTO.logs,
options: { selectColumns: shownColumns, ...formatting, maxLines: 5 },
}),
).toBe(true);
@@ -198,7 +193,6 @@ describe('hasUnsavedViewChanges', () => {
view: bare,
stagedQuery: stagedFrom(bare, DataSource.LOGS),
panelType: PANEL_TYPES.LIST,
sourcePage: SavedviewtypesSourceDTO.logs,
options: {
selectColumns: defaultLogsSelectedColumns,
format: 'table',
@@ -242,7 +236,6 @@ describe('hasUnsavedViewChanges', () => {
view: olderView,
stagedQuery: stagedFrom(olderView, DataSource.METRICS),
panelType: PANEL_TYPES.TIME_SERIES,
sourcePage: SavedviewtypesSourceDTO.metrics,
}),
).toBe(false);
});
@@ -290,7 +283,6 @@ describe('hasUnsavedViewChanges', () => {
view: storedView,
stagedQuery: stagedWithTag,
panelType: PANEL_TYPES.TIME_SERIES,
sourcePage: SavedviewtypesSourceDTO.metrics,
}),
).toBe(false);
});
@@ -330,7 +322,6 @@ describe('hasUnsavedViewChanges', () => {
view: noOrder,
stagedQuery: stagedFrom(noOrder, DataSource.LOGS),
panelType: PANEL_TYPES.LIST,
sourcePage: SavedviewtypesSourceDTO.logs,
}),
).toBe(false);
});
@@ -346,7 +337,6 @@ describe('hasUnsavedViewChanges', () => {
view: noOrder,
stagedQuery: sorted,
panelType: PANEL_TYPES.LIST,
sourcePage: SavedviewtypesSourceDTO.logs,
}),
).toBe(true);
@@ -356,7 +346,6 @@ describe('hasUnsavedViewChanges', () => {
view: saved,
stagedQuery: stagedFrom(saved, DataSource.LOGS),
panelType: PANEL_TYPES.LIST,
sourcePage: SavedviewtypesSourceDTO.logs,
}),
).toBe(false);
});
@@ -373,7 +362,6 @@ describe('hasUnsavedViewChanges', () => {
view: legacy,
stagedQuery: stagedFrom(legacy, DataSource.LOGS),
panelType: PANEL_TYPES.LIST,
sourcePage: SavedviewtypesSourceDTO.logs,
}),
).toBe(false);
});
@@ -385,7 +373,6 @@ describe('hasUnsavedViewChanges', () => {
view,
stagedQuery: staged,
panelType: PANEL_TYPES.LIST,
sourcePage: SavedviewtypesSourceDTO.traces,
options: { ...options, ...formatting, maxLines: 5 },
}),
).toBe(false);

View File

@@ -1,7 +1,4 @@
import {
SavedviewtypesSavedViewSpecDTO,
SavedviewtypesSourceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { SavedviewtypesSavedViewSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { TelemetryFieldKey } from 'api/v5/v5';
import {
defaultLogsSelectedColumns,
@@ -10,6 +7,7 @@ import {
} from 'container/OptionsMenu/constants';
import { FontSize, LogViewMode } from 'container/OptionsMenu/types';
import { Preferences } from 'providers/preferences/types';
import { DataSource } from 'types/common/queryBuilder';
function withColumnNames(
columns: TelemetryFieldKey[] | undefined,
@@ -27,11 +25,11 @@ function withColumnNames(
// defaults filled in where the view has none.
export function getViewColumnsAndFormatting(
spec: SavedviewtypesSavedViewSpecDTO | undefined,
sourcePage: SavedviewtypesSourceDTO,
dataSource: DataSource,
): Preferences {
const selectedFields = spec?.selectedFields as TelemetryFieldKey[] | undefined;
if (sourcePage === SavedviewtypesSourceDTO.logs) {
if (dataSource === DataSource.LOGS) {
return {
columns: ensureLogsRequiredColumns(
withColumnNames(selectedFields) || defaultLogsSelectedColumns,
@@ -44,7 +42,7 @@ export function getViewColumnsAndFormatting(
},
};
}
if (sourcePage === SavedviewtypesSourceDTO.traces) {
if (dataSource === DataSource.TRACES) {
return { columns: selectedFields || defaultTraceSelectedColumns };
}
return { columns: [] };

View File

@@ -1,9 +1,7 @@
import {
SavedviewtypesSourceDTO,
TelemetrytypesFieldContextDTO,
} from 'api/generated/services/sigNoz.schemas';
import { TelemetrytypesFieldContextDTO } from 'api/generated/services/sigNoz.schemas';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import isEqual from 'lodash-es/isEqual';
import { DataSource } from 'types/common/queryBuilder';
import { HasUnsavedViewChangesArgs } from '../types';
import { getSavedViewQuery } from './getSavedViewQuery';
@@ -52,7 +50,6 @@ export function hasUnsavedViewChanges({
stagedQuery,
panelType,
options,
sourcePage,
}: HasUnsavedViewChangesArgs): boolean {
if (!stagedQuery) {
return false;
@@ -79,9 +76,10 @@ export function hasUnsavedViewChanges({
return false;
}
const dataSource = stagedQuery.builder.queryData[0]?.dataSource;
const { columns, formatting } = getViewColumnsAndFormatting(
view.spec,
sourcePage,
dataSource,
);
const toNames = (fields: { name?: string; key?: string }[]): unknown[] =>
fields.map((field) => field.name ?? field.key);
@@ -89,7 +87,7 @@ export function hasUnsavedViewChanges({
return true;
}
if (sourcePage !== SavedviewtypesSourceDTO.logs || !formatting) {
if (dataSource !== DataSource.LOGS || !formatting) {
return false;
}
return (

View File

@@ -3,8 +3,11 @@ import { DataSource } from 'types/common/queryBuilder';
import { SavedViewSourcePage } from '../types';
// The AI assistant names a view's source by DataSource (the signal), the api
// keys views by source page.
// Explorers and the preferences module are keyed by DataSource (the signal),
// the api keys views by source page. Same values today, so this is the one
// place they meet. AI observability views will come with their own source and
// DataSource cannot tell them apart from traces, so preferences should move to
// source page at that point and this map goes with it.
const SAVED_VIEW_SOURCE: Record<SavedViewSourcePage, SavedviewtypesSourceDTO> =
{
[DataSource.LOGS]: SavedviewtypesSourceDTO.logs,

View File

@@ -11,7 +11,6 @@ import {
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import logEvent from 'api/common/logEvent';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
import type { TableColumnDef } from 'components/TanStackTableView/types';
@@ -87,7 +86,7 @@ function ListView({
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.TRACES_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
aggregateOperator: 'count',
initialOptions: {
selectColumns: defaultSelectedColumns,

View File

@@ -73,7 +73,7 @@ function TracesExplorer(): JSX.Element {
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.TRACES_LIST_OPTIONS,
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
aggregateOperator: 'noop',
initialOptions: {
selectColumns: defaultSelectedColumns,

View File

@@ -1,15 +1,15 @@
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { TelemetryFieldKey } from 'api/v5/v5';
import { LOCALSTORAGE } from 'constants/localStorage';
import { LogViewMode, FontSize } from 'container/OptionsMenu/types';
import { LogViewMode } from 'container/OptionsMenu/types';
import {
defaultLogsSelectedColumns,
defaultTraceSelectedColumns,
} from 'container/OptionsMenu/constants';
import { FontSize } from 'container/OptionsMenu/types';
import { render, screen, userEvent } from 'tests/test-utils';
import { DataSource } from 'types/common/queryBuilder';
import { usePreferenceContext } from '../context/PreferenceContextProvider';
import { PreferenceSourcePage } from '../types';
const ROUTE_LOGS = '/logs';
const ROUTE_TRACES = '/traces';
@@ -38,13 +38,14 @@ function getLocalStorageJSON<T>(key: string): T | null {
}
function Consumer({
sourcePage,
dataSource,
testIdPrefix,
}: {
sourcePage: PreferenceSourcePage;
dataSource: DataSource;
testIdPrefix: string;
}): JSX.Element {
const slice = usePreferenceContext()[sourcePage];
const ctx = usePreferenceContext();
const slice = dataSource === DataSource.TRACES ? ctx.traces : ctx.logs;
return (
<div>
@@ -57,7 +58,7 @@ function Consumer({
type="button"
onClick={(): void => {
const newCols: TelemetryFieldKey[] =
sourcePage === SavedviewtypesSourceDTO.traces
dataSource === DataSource.TRACES
? (defaultTraceSelectedColumns.slice(0, 1) as TelemetryFieldKey[])
: (defaultLogsSelectedColumns.slice(0, 1) as TelemetryFieldKey[]);
slice.updateColumns(newCols);
@@ -77,10 +78,7 @@ describe('PreferencesProvider integration', () => {
describe('Logs', () => {
it('loads defaults when no localStorage or url provided', () => {
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.logs}
testIdPrefix={TESTID_LOGS}
/>,
<Consumer dataSource={DataSource.LOGS} testIdPrefix={TESTID_LOGS} />,
undefined,
{
initialRoute: ROUTE_LOGS,
@@ -103,10 +101,7 @@ describe('PreferencesProvider integration', () => {
});
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.logs}
testIdPrefix={TESTID_LOGS}
/>,
<Consumer dataSource={DataSource.LOGS} testIdPrefix={TESTID_LOGS} />,
undefined,
{
initialRoute: ROUTE_LOGS,
@@ -121,10 +116,7 @@ describe('PreferencesProvider integration', () => {
it('direct mode updateColumns persists to localStorage', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.logs}
testIdPrefix={TESTID_LOGS}
/>,
<Consumer dataSource={DataSource.LOGS} testIdPrefix={TESTID_LOGS} />,
undefined,
{
initialRoute: ROUTE_LOGS,
@@ -150,7 +142,7 @@ describe('PreferencesProvider integration', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<Consumer sourcePage={SavedviewtypesSourceDTO.logs} testIdPrefix="logs" />,
<Consumer dataSource={DataSource.LOGS} testIdPrefix="logs" />,
undefined,
{
initialRoute: initialEntry,
@@ -183,10 +175,7 @@ describe('PreferencesProvider integration', () => {
});
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.logs}
testIdPrefix={TESTID_LOGS}
/>,
<Consumer dataSource={DataSource.LOGS} testIdPrefix={TESTID_LOGS} />,
undefined,
{
initialRoute: ROUTE_LOGS,
@@ -208,7 +197,7 @@ describe('PreferencesProvider integration', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
function FormattingConsumer(): JSX.Element {
const logs = usePreferenceContext()[SavedviewtypesSourceDTO.logs];
const { logs } = usePreferenceContext();
return (
<button
data-testid="logs-update-formatting"
@@ -246,10 +235,7 @@ describe('PreferencesProvider integration', () => {
const initialEntry = `/logs?viewKey=${encodeURIComponent(viewKey)}`;
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.logs}
testIdPrefix={TESTID_LOGS}
/>,
<Consumer dataSource={DataSource.LOGS} testIdPrefix={TESTID_LOGS} />,
undefined,
{ initialRoute: initialEntry },
);
@@ -268,10 +254,7 @@ describe('PreferencesProvider integration', () => {
describe('Traces', () => {
it('loads defaults when no localStorage or url provided', () => {
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.traces}
testIdPrefix={TESTID_TRACES}
/>,
<Consumer dataSource={DataSource.TRACES} testIdPrefix={TESTID_TRACES} />,
undefined,
{
initialRoute: ROUTE_TRACES,
@@ -290,10 +273,7 @@ describe('PreferencesProvider integration', () => {
});
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.traces}
testIdPrefix={TESTID_TRACES}
/>,
<Consumer dataSource={DataSource.TRACES} testIdPrefix={TESTID_TRACES} />,
undefined,
{
initialRoute: ROUTE_TRACES,
@@ -306,10 +286,7 @@ describe('PreferencesProvider integration', () => {
it('direct mode updateColumns persists to localStorage', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.traces}
testIdPrefix={TESTID_TRACES}
/>,
<Consumer dataSource={DataSource.TRACES} testIdPrefix={TESTID_TRACES} />,
undefined,
{
initialRoute: ROUTE_TRACES,
@@ -332,10 +309,7 @@ describe('PreferencesProvider integration', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.traces}
testIdPrefix="traces"
/>,
<Consumer dataSource={DataSource.TRACES} testIdPrefix="traces" />,
undefined,
{
initialRoute: initialEntry,
@@ -364,10 +338,7 @@ describe('PreferencesProvider integration', () => {
});
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.traces}
testIdPrefix={TESTID_TRACES}
/>,
<Consumer dataSource={DataSource.TRACES} testIdPrefix={TESTID_TRACES} />,
undefined,
{ initialRoute: ROUTE_TRACES },
);
@@ -384,7 +355,7 @@ describe('PreferencesProvider integration', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
function TracesFormattingConsumer(): JSX.Element {
const traces = usePreferenceContext()[SavedviewtypesSourceDTO.traces];
const { traces } = usePreferenceContext();
return (
<button
data-testid="traces-update-formatting"
@@ -419,10 +390,7 @@ describe('PreferencesProvider integration', () => {
const initialEntry = `/traces?viewKey=${encodeURIComponent(viewKey)}`;
render(
<Consumer
sourcePage={SavedviewtypesSourceDTO.traces}
testIdPrefix={TESTID_TRACES}
/>,
<Consumer dataSource={DataSource.TRACES} testIdPrefix={TESTID_TRACES} />,
undefined,
{ initialRoute: initialEntry },
);

View File

@@ -1,6 +1,6 @@
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { renderHook, waitFor } from '@testing-library/react';
import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants';
import { DataSource } from 'types/common/queryBuilder';
import logsLoaderConfig from '../configs/logsLoaderConfig';
import { usePreferenceLoader } from '../loader/usePreferenceLoader';
@@ -50,7 +50,7 @@ describe('usePreferenceLoader', () => {
const setReSync = jest.fn();
const { result } = renderHook(() =>
usePreferenceLoader({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
reSync: false,
setReSync,
}),
@@ -74,7 +74,7 @@ describe('usePreferenceLoader', () => {
const setReSync = jest.fn();
const { result } = renderHook(() =>
usePreferenceLoader({
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
reSync: false,
setReSync,
}),
@@ -99,7 +99,7 @@ describe('usePreferenceLoader', () => {
// We'll unmount quickly to avoid the infinite loop
const { unmount } = renderHook(() =>
usePreferenceLoader({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
reSync: true,
setReSync,
}),
@@ -130,7 +130,7 @@ describe('usePreferenceLoader', () => {
const setReSync = jest.fn();
const { result } = renderHook(() =>
usePreferenceLoader({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
reSync: true,
setReSync,
}),

View File

@@ -9,6 +9,7 @@ import {
defaultTraceSelectedColumns,
ensureLogsRequiredColumns,
} from 'container/OptionsMenu/constants';
import { DataSource } from 'types/common/queryBuilder';
import { usePreferenceSync } from '../sync/usePreferenceSync';
import { PreferenceMode } from '../types';
@@ -83,7 +84,7 @@ describe('usePreferenceSync in saved view mode', () => {
renderHook(() =>
usePreferenceSync({
mode: PreferenceMode.DIRECT,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
savedViewId: undefined,
}),
);
@@ -100,7 +101,7 @@ describe('usePreferenceSync in saved view mode', () => {
const { result } = renderHook(() =>
usePreferenceSync({
mode: PreferenceMode.DIRECT,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
savedViewId: undefined,
}),
);
@@ -119,7 +120,7 @@ describe('usePreferenceSync in saved view mode', () => {
const { result } = renderHook(() =>
usePreferenceSync({
mode: PreferenceMode.SAVED_VIEW,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
savedViewId: 'view-1',
}),
);
@@ -146,7 +147,7 @@ describe('usePreferenceSync in saved view mode', () => {
const { result } = renderHook(() =>
usePreferenceSync({
mode: PreferenceMode.SAVED_VIEW,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
savedViewId: 'view-1',
}),
);
@@ -173,14 +174,14 @@ describe('usePreferenceSync in saved view mode', () => {
const withFields = renderHook(() =>
usePreferenceSync({
mode: PreferenceMode.SAVED_VIEW,
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
savedViewId: 'with-fields',
}),
);
const withoutFields = renderHook(() =>
usePreferenceSync({
mode: PreferenceMode.SAVED_VIEW,
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
savedViewId: 'without-fields',
}),
);
@@ -201,7 +202,7 @@ describe('usePreferenceSync in saved view mode', () => {
const { result } = renderHook(() =>
usePreferenceSync({
mode: PreferenceMode.SAVED_VIEW,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
savedViewId: 'missing',
}),
);

View File

@@ -1,36 +1,35 @@
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { act } from 'react-dom/test-utils';
import { renderHook } from '@testing-library/react';
import { TelemetryFieldKey } from 'api/v5/v5';
import { LogViewMode, FontSize } from 'container/OptionsMenu/types';
import { LogViewMode } from 'container/OptionsMenu/types';
import { FontSize } from 'container/OptionsMenu/types';
import {
FormattingOptions,
PreferenceMode,
Preferences,
} from 'providers/preferences/types';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import { usePreferenceUpdater } from '../updater/usePreferenceUpdater';
// Mock the config updaters
const mockLogsUpdateColumns = jest.fn();
const mockLogsUpdateFormatting = jest.fn();
const mockTracesUpdateColumns = jest.fn();
const mockTracesUpdateFormatting = jest.fn();
const mockUpdateColumns = jest.fn();
const mockUpdateFormatting = jest.fn();
jest.mock('../configs/logsUpdaterConfig', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
updateColumns: mockLogsUpdateColumns,
updateFormatting: mockLogsUpdateFormatting,
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
})),
}));
jest.mock('../configs/tracesUpdaterConfig', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
updateColumns: mockTracesUpdateColumns,
updateFormatting: mockTracesUpdateFormatting,
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
})),
}));
@@ -63,7 +62,7 @@ describe('usePreferenceUpdater', () => {
const { result } = renderHook(() =>
usePreferenceUpdater({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
mode: PreferenceMode.DIRECT,
preferences: mockPreferences,
setReSync,
@@ -76,7 +75,7 @@ describe('usePreferenceUpdater', () => {
expect(typeof result.current.updateFormatting).toBe('function');
});
it('should call the logs updater for updateColumns with the logs list key', () => {
it('should call the logs updater for updateColumns with logs dataSource', () => {
const setReSync = jest.fn();
const setSavedViewPreferences = jest.fn();
const newColumns: TelemetryFieldKey[] = [
@@ -89,7 +88,7 @@ describe('usePreferenceUpdater', () => {
const { result } = renderHook(() =>
usePreferenceUpdater({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
mode: PreferenceMode.DIRECT,
preferences: mockPreferences,
setReSync,
@@ -102,15 +101,14 @@ describe('usePreferenceUpdater', () => {
});
// Should call the logs updater
expect(mockLogsUpdateColumns).toHaveBeenCalledWith(
expect(mockUpdateColumns).toHaveBeenCalledWith(
newColumns,
PreferenceMode.DIRECT,
);
expect(mockTracesUpdateColumns).not.toHaveBeenCalled();
expect(setReSync).toHaveBeenCalledWith(true);
});
it('should call the logs updater for updateFormatting with the logs list key', () => {
it('should call the logs updater for updateFormatting with logs dataSource', () => {
const setReSync = jest.fn();
const setSavedViewPreferences = jest.fn();
const newFormatting: FormattingOptions = {
@@ -122,7 +120,7 @@ describe('usePreferenceUpdater', () => {
const { result } = renderHook(() =>
usePreferenceUpdater({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
mode: PreferenceMode.DIRECT,
preferences: mockPreferences,
setReSync,
@@ -135,15 +133,14 @@ describe('usePreferenceUpdater', () => {
});
// Should call the logs updater
expect(mockLogsUpdateFormatting).toHaveBeenCalledWith(
expect(mockUpdateFormatting).toHaveBeenCalledWith(
newFormatting,
PreferenceMode.DIRECT,
);
expect(mockTracesUpdateFormatting).not.toHaveBeenCalled();
expect(setReSync).toHaveBeenCalledWith(true);
});
it('should call the traces updater for updateColumns with the traces list key', () => {
it('should call the traces updater for updateColumns with traces dataSource', () => {
const setReSync = jest.fn();
const setSavedViewPreferences = jest.fn();
const newColumns: TelemetryFieldKey[] = [
@@ -156,7 +153,7 @@ describe('usePreferenceUpdater', () => {
const { result } = renderHook(() =>
usePreferenceUpdater({
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
mode: PreferenceMode.DIRECT,
preferences: mockPreferences,
setReSync,
@@ -169,15 +166,14 @@ describe('usePreferenceUpdater', () => {
});
// Should call the traces updater
expect(mockTracesUpdateColumns).toHaveBeenCalledWith(
expect(mockUpdateColumns).toHaveBeenCalledWith(
newColumns,
PreferenceMode.DIRECT,
);
expect(mockLogsUpdateColumns).not.toHaveBeenCalled();
expect(setReSync).toHaveBeenCalledWith(true);
});
it('should call the traces updater for updateFormatting with the traces list key', () => {
it('should call the traces updater for updateFormatting with traces dataSource', () => {
const setReSync = jest.fn();
const setSavedViewPreferences = jest.fn();
const newFormatting: FormattingOptions = {
@@ -189,7 +185,7 @@ describe('usePreferenceUpdater', () => {
const { result } = renderHook(() =>
usePreferenceUpdater({
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
mode: PreferenceMode.DIRECT,
preferences: mockPreferences,
setReSync,
@@ -202,11 +198,10 @@ describe('usePreferenceUpdater', () => {
});
// Should call the traces updater
expect(mockTracesUpdateFormatting).toHaveBeenCalledWith(
expect(mockUpdateFormatting).toHaveBeenCalledWith(
newFormatting,
PreferenceMode.DIRECT,
);
expect(mockLogsUpdateFormatting).not.toHaveBeenCalled();
expect(setReSync).toHaveBeenCalledWith(true);
});
@@ -216,7 +211,7 @@ describe('usePreferenceUpdater', () => {
const { result } = renderHook(() =>
usePreferenceUpdater({
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
mode: PreferenceMode.DIRECT,
preferences: mockPreferences,
setReSync,

View File

@@ -1,20 +0,0 @@
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { isPreferenceSourcePage } from '../utils';
describe('isPreferenceSourcePage', () => {
it.each([SavedviewtypesSourceDTO.logs, SavedviewtypesSourceDTO.traces])(
'is true for %s',
(source) => {
expect(isPreferenceSourcePage(source)).toBe(true);
},
);
it.each([
SavedviewtypesSourceDTO.metrics,
SavedviewtypesSourceDTO.meter,
SavedviewtypesSourceDTO.ai_observability,
])('is false for %s', (source) => {
expect(isPreferenceSourcePage(source)).toBe(false);
});
});

View File

@@ -1,7 +0,0 @@
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
// Only the logs and traces explorers keep column and formatting preferences.
export const PREFERENCE_SOURCE_PAGES = [
SavedviewtypesSourceDTO.logs,
SavedviewtypesSourceDTO.traces,
] as const;

View File

@@ -5,12 +5,12 @@ import React, {
useContext,
useMemo,
} from 'react';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import useUrlQuery from 'hooks/useUrlQuery';
import {
PreferenceContextValue,
PreferenceMode,
} from 'providers/preferences/types';
import { DataSource } from 'types/common/queryBuilder';
import { usePreferenceSync } from '../sync/usePreferenceSync';
@@ -38,19 +38,19 @@ export function PreferenceContextProvider({
const logsSlice = usePreferenceSync({
mode: savedViewId ? PreferenceMode.SAVED_VIEW : PreferenceMode.DIRECT,
savedViewId: savedViewId || undefined,
sourcePage: SavedviewtypesSourceDTO.logs,
dataSource: DataSource.LOGS,
});
const tracesSlice = usePreferenceSync({
mode: savedViewId ? PreferenceMode.SAVED_VIEW : PreferenceMode.DIRECT,
savedViewId: savedViewId || undefined,
sourcePage: SavedviewtypesSourceDTO.traces,
dataSource: DataSource.TRACES,
});
const value = useMemo<PreferenceContextValue>(
() => ({
[SavedviewtypesSourceDTO.logs]: logsSlice,
[SavedviewtypesSourceDTO.traces]: tracesSlice,
logs: logsSlice,
traces: tracesSlice,
}),
[logsSlice, tracesSlice],
);

View File

@@ -1,12 +0,0 @@
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { PreferenceSlice } from '../types';
import { isPreferenceSourcePage } from '../utils';
import { usePreferenceContext } from './PreferenceContextProvider';
export function usePreferenceSlice(
sourcePage: SavedviewtypesSourceDTO,
): PreferenceSlice | undefined {
const context = usePreferenceContext();
return isPreferenceSourcePage(sourcePage) ? context[sourcePage] : undefined;
}

View File

@@ -1,12 +1,12 @@
import { useEffect, useState } from 'react';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { TelemetryFieldKey } from 'api/v5/v5';
import { ensureLogsRequiredColumns } from 'container/OptionsMenu/constants';
import { has } from 'lodash-es';
import { DataSource } from 'types/common/queryBuilder';
import logsLoaderConfig from '../configs/logsLoaderConfig';
import tracesLoaderConfig from '../configs/tracesLoaderConfig';
import { FormattingOptions, Preferences, PreferenceSourcePage } from '../types';
import { FormattingOptions, Preferences } from '../types';
const migrateColumns = (columns: any): any =>
columns.map((column: any) => {
@@ -62,21 +62,15 @@ function logsPreferencesLoader(): {
function tracesPreferencesLoader(): {
columns: TelemetryFieldKey[];
} {
const { columns } = preferencesLoader<Preferences>(tracesLoaderConfig);
return { columns };
return preferencesLoader(tracesLoaderConfig);
}
const PREFERENCE_LOADERS: Record<PreferenceSourcePage, () => Preferences> = {
[SavedviewtypesSourceDTO.logs]: logsPreferencesLoader,
[SavedviewtypesSourceDTO.traces]: tracesPreferencesLoader,
};
export function usePreferenceLoader({
sourcePage,
dataSource,
reSync,
setReSync,
}: {
sourcePage: PreferenceSourcePage;
dataSource: DataSource;
reSync: boolean;
setReSync: (value: boolean) => void;
}): {
@@ -84,10 +78,17 @@ export function usePreferenceLoader({
loading: boolean;
error: Error | null;
} {
const loadSlicePreferences = PREFERENCE_LOADERS[sourcePage];
const [preferences, setPreferences] = useState<Preferences | null>(
loadSlicePreferences,
);
const [preferences, setPreferences] = useState<Preferences | null>(() => {
if (dataSource === DataSource.LOGS) {
const { columns, formatting } = logsPreferencesLoader();
return { columns, formatting };
}
if (dataSource === DataSource.TRACES) {
const { columns } = tracesPreferencesLoader();
return { columns };
}
return null;
});
const [error, setError] = useState<Error | null>(null);
useEffect((): void => {
@@ -95,7 +96,15 @@ export function usePreferenceLoader({
setError(null);
try {
setPreferences(loadSlicePreferences());
if (dataSource === DataSource.LOGS) {
const { columns, formatting } = logsPreferencesLoader();
setPreferences({ columns, formatting });
}
if (dataSource === DataSource.TRACES) {
const { columns } = tracesPreferencesLoader();
setPreferences({ columns });
}
} catch (e) {
setError(e as Error);
} finally {
@@ -110,7 +119,7 @@ export function usePreferenceLoader({
if (reSync) {
loadPreferences();
}
}, [loadSlicePreferences, reSync, setReSync]);
}, [dataSource, reSync, setReSync]);
return { preferences, loading: false, error };
}

View File

@@ -3,23 +3,19 @@ import { useEffect, useRef, useState } from 'react';
import { useGetSavedView } from 'api/generated/services/saved-view';
import { TelemetryFieldKey } from 'api/v5/v5';
import { getViewColumnsAndFormatting } from 'container/SavedViews/utils/getViewColumnsAndFormatting';
import { DataSource } from 'types/common/queryBuilder';
import { usePreferenceLoader } from '../loader/usePreferenceLoader';
import {
FormattingOptions,
PreferenceMode,
Preferences,
PreferenceSourcePage,
} from '../types';
import { FormattingOptions, PreferenceMode, Preferences } from '../types';
import { usePreferenceUpdater } from '../updater/usePreferenceUpdater';
export function usePreferenceSync({
mode,
sourcePage,
dataSource,
savedViewId,
}: {
mode: PreferenceMode;
sourcePage: PreferenceSourcePage;
dataSource: DataSource;
savedViewId: string | undefined;
}): {
preferences: Preferences | null;
@@ -45,25 +41,25 @@ export function usePreferenceSync({
if (view && view.id === savedViewId) {
if (seededViewId.current !== savedViewId) {
seededViewId.current = savedViewId;
setSavedViewPreferences(getViewColumnsAndFormatting(view.spec, sourcePage));
setSavedViewPreferences(getViewColumnsAndFormatting(view.spec, dataSource));
}
return;
}
seededViewId.current = undefined;
setSavedViewPreferences(getViewColumnsAndFormatting(undefined, sourcePage));
}, [viewData, sourcePage, savedViewId]);
setSavedViewPreferences(getViewColumnsAndFormatting(undefined, dataSource));
}, [viewData, dataSource, savedViewId]);
// We are using a reSync state because we have URL updates as well as local storage updates
// and we want to make sure we are always using the latest preferences
const [reSync, setReSync] = useState(false);
const { preferences, loading, error } = usePreferenceLoader({
sourcePage,
dataSource,
reSync,
setReSync,
});
const { updateColumns, updateFormatting } = usePreferenceUpdater({
sourcePage,
dataSource,
mode,
preferences,
setReSync,

View File

@@ -1,15 +1,12 @@
import { TelemetryFieldKey } from 'api/v5/v5';
import { LogViewMode, FontSize } from 'container/OptionsMenu/types';
import { PREFERENCE_SOURCE_PAGES } from '../constants';
import { LogViewMode } from 'container/OptionsMenu/types';
import { FontSize } from 'container/OptionsMenu/types';
export enum PreferenceMode {
SAVED_VIEW = 'savedView',
DIRECT = 'direct',
}
export type PreferenceSourcePage = (typeof PREFERENCE_SOURCE_PAGES)[number];
export interface PreferenceSlice {
preferences: Preferences | null;
loading: boolean;
@@ -18,10 +15,10 @@ export interface PreferenceSlice {
updateFormatting: (newFormatting: FormattingOptions) => void;
}
export type PreferenceContextValue = Record<
PreferenceSourcePage,
PreferenceSlice
>;
export interface PreferenceContextValue {
logs: PreferenceSlice;
traces: PreferenceSlice;
}
export interface FormattingOptions {
maxLines?: number;

View File

@@ -1,5 +1,4 @@
import { Dispatch, SetStateAction } from 'react';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { TelemetryFieldKey } from 'api/v5/v5';
import {
defaultOptionsQuery,
@@ -7,41 +6,48 @@ import {
} from 'container/OptionsMenu/constants';
import { OptionsQuery } from 'container/OptionsMenu/types';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { DataSource } from 'types/common/queryBuilder';
import getLogsUpdaterConfig from '../configs/logsUpdaterConfig';
import getTracesUpdaterConfig from '../configs/tracesUpdaterConfig';
import { FormattingOptions, Preferences, PreferenceSourcePage } from '../types';
import { FormattingOptions, Preferences } from '../types';
const metricsUpdater = {
updateColumns: (): void => {}, // no-op for metrics
updateFormatting: (): void => {}, // no-op for metrics
};
const getUpdaterConfig = (
preferences: Preferences | null,
redirectWithOptionsData: (options: OptionsQuery) => void,
setSavedViewPreferences: Dispatch<SetStateAction<Preferences | null>>,
): Record<
PreferenceSourcePage,
DataSource,
{
updateColumns: (newColumns: TelemetryFieldKey[], mode: string) => void;
updateFormatting: (newFormatting: FormattingOptions, mode: string) => void;
}
> => ({
[SavedviewtypesSourceDTO.logs]: getLogsUpdaterConfig(
[DataSource.LOGS]: getLogsUpdaterConfig(
preferences,
redirectWithOptionsData,
setSavedViewPreferences,
),
[SavedviewtypesSourceDTO.traces]: getTracesUpdaterConfig(
[DataSource.TRACES]: getTracesUpdaterConfig(
redirectWithOptionsData,
setSavedViewPreferences,
),
[DataSource.METRICS]: metricsUpdater,
});
export function usePreferenceUpdater({
sourcePage,
dataSource,
mode,
preferences,
setReSync,
setSavedViewPreferences,
}: {
sourcePage: PreferenceSourcePage;
dataSource: DataSource;
mode: string;
preferences: Preferences | null;
setReSync: Dispatch<SetStateAction<boolean>>;
@@ -56,7 +62,7 @@ export function usePreferenceUpdater({
preferences,
redirectWithOptionsData,
setSavedViewPreferences,
)[sourcePage];
)[dataSource];
return {
updateColumns: (newColumns: TelemetryFieldKey[]): void => {

View File

@@ -1,12 +0,0 @@
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { PREFERENCE_SOURCE_PAGES } from './constants';
import { PreferenceSourcePage } from './types';
export function isPreferenceSourcePage(
sourcePage: SavedviewtypesSourceDTO,
): sourcePage is PreferenceSourcePage {
return (
PREFERENCE_SOURCE_PAGES as readonly SavedviewtypesSourceDTO[]
).includes(sourcePage);
}