Compare commits

...

11 Commits

Author SHA1 Message Date
amlannandy
3660b289c3 chore: fix test 2025-11-27 10:04:35 +07:00
amlannandy
a69da7cacf chore: update all option text 2025-11-26 17:52:09 +07:00
amlannandy
8d7a3cea9f chore: ease in disabling grouping in notification settings 2025-11-26 16:09:52 +07:00
SagarRajput-7
c904ab5d99 fix: updated playwright to patch ssl certificate verification vulnerability (#9664) 2025-11-25 09:36:12 +00:00
SagarRajput-7
d53f9a7e16 fix: removed the decimal places logic from getYAxisFormattedValue (#9537)
* fix: fix typeerror in getYAxisFormattedValue function

* fix: added test cases

* fix: added format equals none handling in try-catch

* fix: test cleanup
2025-11-25 09:22:24 +00:00
Vishal Sharma
1b01b61026 chore: remove userpilot and update Posthog (#9668) 2025-11-24 23:51:40 +05:30
Vishal Sharma
95a26cecba feat: Introduce PYLON_IDENTITY_SECRET environment variable (#9656) 2025-11-24 14:54:37 +00:00
Shaheer Kochai
15af828005 fix: external APIs page bugfixes / improvements (#9586)
* style: fix the UI issues in endpoint metadata pills

* style: fix the UI issues in endpoint and QB filters

* fix: fix the light mode colors for domain drawer

* fix: fix datatype and type-tag pills breaking for smaller width QB search

* style: enhance text overflow handling in QueryBuilder search options

* fix: remove visible 'View Traces' buttons on drag selection in UPlot chart options

* fix: add border-bottom to table cells when pagination is not present

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2025-11-24 13:32:54 +00:00
Vishal Sharma
e5b99703ac Chore/user email log event (#9655)
* feat: update logEvent, rename `tenant_url` to `deployment_url`

* feat: Update telemetry attributes, add logs format options tooltip, enable login form submission with Enter

* test: update test
2025-11-24 12:21:04 +00:00
Tushar Vats
f0941c7b2e fix: added ttl for logs_attribute_keys, logs_resource_keys and span_attributes_keys (#9545)
* fix: added ttl for logs_attribute_keys, logs_resource_keys and span_attributes_keys

* fix: table name consitent

* fix: table name

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: typo

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: ttl query for retention

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-24 17:16:07 +05:30
Nityananda Gohain
12c9b921a7 chore: fix error in http_handler for get ttl (#9652) 2025-11-22 14:47:34 +05:30
25 changed files with 450 additions and 470 deletions

View File

@@ -3,5 +3,6 @@ BUNDLE_ANALYSER="true"
FRONTEND_API_ENDPOINT="http://localhost:8080/"
PYLON_APP_ID="pylon-app-id"
APPCUES_APP_ID="appcess-app-id"
PYLON_IDENTITY_SECRET="pylon-identity-secret"
CI="1"

View File

@@ -38,7 +38,7 @@
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
"@monaco-editor/react": "^4.3.1",
"@playwright/test": "1.54.1",
"@playwright/test": "1.55.1",
"@radix-ui/react-tabs": "1.0.4",
"@radix-ui/react-tooltip": "1.0.7",
"@sentry/react": "8.41.0",
@@ -83,6 +83,7 @@
"color": "^4.2.1",
"color-alpha": "1.1.3",
"cross-env": "^7.0.3",
"crypto-js": "4.2.0",
"css-loader": "5.0.0",
"css-minimizer-webpack-plugin": "5.0.1",
"d3-hierarchy": "3.1.2",
@@ -112,7 +113,7 @@
"overlayscrollbars": "^2.8.1",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
"posthog-js": "1.215.5",
"posthog-js": "1.298.0",
"rc-tween-one": "3.0.6",
"react": "18.2.0",
"react-addons-update": "15.6.3",
@@ -149,7 +150,6 @@
"tsconfig-paths-webpack-plugin": "^3.5.1",
"typescript": "^4.0.5",
"uplot": "1.6.31",
"userpilot": "1.3.9",
"uuid": "^8.3.2",
"web-vitals": "^0.2.4",
"webpack": "5.94.0",
@@ -186,6 +186,7 @@
"@types/color": "^3.0.3",
"@types/compression-webpack-plugin": "^9.0.0",
"@types/copy-webpack-plugin": "^8.0.1",
"@types/crypto-js": "4.2.2",
"@types/dompurify": "^2.4.0",
"@types/event-source-polyfill": "^1.0.0",
"@types/fontfaceobserver": "2.1.0",

View File

@@ -7,11 +7,12 @@ import AppLoading from 'components/AppLoading/AppLoading';
import KBarCommandPalette from 'components/KBarCommandPalette/KBarCommandPalette';
import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner';
import UserpilotRouteTracker from 'components/UserpilotRouteTracker/UserpilotRouteTracker';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import AppLayout from 'container/AppLayout';
import Hex from 'crypto-js/enc-hex';
import HmacSHA256 from 'crypto-js/hmac-sha256';
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useThemeConfig } from 'hooks/useDarkMode';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
@@ -33,7 +34,6 @@ import { Suspense, useCallback, useEffect, useState } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
import { CompatRouter } from 'react-router-dom-v5-compat';
import { LicenseStatus } from 'types/api/licensesV3/getActive';
import { Userpilot } from 'userpilot';
import { extractDomain } from 'utils/app';
import { Home } from './pageComponents';
@@ -84,9 +84,9 @@ function App(): JSX.Element {
email,
name: displayName,
company_name: orgName,
tenant_id: hostNameParts[0],
deployment_name: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
deployment_url: hostname,
company_domain: domain,
source: 'signoz-ui',
role,
@@ -94,9 +94,9 @@ function App(): JSX.Element {
const groupTraits = {
name: orgName,
tenant_id: hostNameParts[0],
deployment_name: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
deployment_url: hostname,
company_domain: domain,
source: 'signoz-ui',
};
@@ -111,37 +111,23 @@ function App(): JSX.Element {
if (window && window.Appcues) {
window.Appcues.identify(id, {
name: displayName,
tenant_id: hostNameParts[0],
deployment_name: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
deployment_url: hostname,
company_domain: domain,
companyName: orgName,
email,
paidUser: !!trialInfo?.trialConvertedToSubscription,
});
}
Userpilot.identify(email, {
email,
name: displayName,
orgName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
});
posthog?.identify(id, {
email,
name: displayName,
orgName,
tenant_id: hostNameParts[0],
deployment_name: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
deployment_url: hostname,
company_domain: domain,
source: 'signoz-ui',
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
@@ -149,9 +135,9 @@ function App(): JSX.Element {
posthog?.group('company', orgId, {
name: orgName,
tenant_id: hostNameParts[0],
deployment_name: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
deployment_url: hostname,
company_domain: domain,
source: 'signoz-ui',
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
@@ -270,11 +256,20 @@ function App(): JSX.Element {
!showAddCreditCardModal &&
(isCloudUser || isEnterpriseSelfHostedUser)
) {
const email = user.email || '';
const secret = process.env.PYLON_IDENTITY_SECRET || '';
let emailHash = '';
if (email && secret) {
emailHash = HmacSHA256(email, Hex.parse(secret)).toString(Hex);
}
window.pylon = {
chat_settings: {
app_id: process.env.PYLON_APP_ID,
email: user.email,
name: user.displayName || user.email,
email_hash: emailHash,
},
};
}
@@ -308,10 +303,6 @@ function App(): JSX.Element {
});
}
if (process.env.USERPILOT_KEY) {
Userpilot.initialize(process.env.USERPILOT_KEY);
}
if (!isSentryInitialized) {
Sentry.init({
dsn: process.env.SENTRY_DSN,
@@ -372,7 +363,6 @@ function App(): JSX.Element {
<Router history={history}>
<CompatRouter>
<KBarCommandPaletteProvider>
<UserpilotRouteTracker />
<KBarCommandPalette />
<NotificationProvider>
<ErrorModalProvider>

View File

@@ -1,6 +1,8 @@
import { LogEventAxiosInstance as axios } from 'api';
import getLocalStorageApi from 'api/browser/localstorage/get';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { LOCALSTORAGE } from 'constants/localStorage';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { EventSuccessPayloadProps } from 'types/api/events/types';
@@ -11,9 +13,14 @@ const logEvent = async (
rateLimited?: boolean,
): Promise<SuccessResponse<EventSuccessPayloadProps> | ErrorResponse> => {
try {
// add tenant_url to attributes
// add deployment_url and user_email to attributes
const { hostname } = window.location;
const updatedAttributes = { ...attributes, tenant_url: hostname };
const userEmail = getLocalStorageApi(LOCALSTORAGE.LOGGED_IN_USER_EMAIL);
const updatedAttributes = {
...attributes,
deployment_url: hostname,
user_email: userEmail,
};
const response = await axios.post('/event', {
eventName,
attributes: updatedAttributes,

View File

@@ -232,7 +232,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
).toBe('1%');
expect(
testFullPrecisionGetYAxisFormattedValue('1.00555555559595876', 'percent'),
).toBe('1.005555555595958%');
).toBe('1.005555555595959%');
});
test('ratio', () => {
@@ -359,7 +359,7 @@ describe('getYAxisFormattedValue - precision option tests', () => {
's',
PrecisionOptionsEnum.FULL,
),
).toBe('26254299141484417000000 µs');
).toBe('26.254299141484417 µs');
expect(
getYAxisFormattedValue('4353.81', 'ms', PrecisionOptionsEnum.FULL),

View File

@@ -101,19 +101,10 @@ export const getYAxisFormattedValue = (
if (numValue === Infinity) return '∞';
if (numValue === -Infinity) return '-∞';
const decimalPlaces = value.split('.')[1]?.length || undefined;
// Use custom formatter for the 'none' format honoring precision
if (format === 'none') {
return formatDecimalWithLeadingZeros(numValue, precision);
}
// For all other standard formats, delegate to grafana/data's built-in formatter.
const computeDecimals = (): number | undefined => {
if (precision === PrecisionOptionsEnum.FULL) {
return decimalPlaces && decimalPlaces >= DEFAULT_SIGNIFICANT_DIGITS
? decimalPlaces
: DEFAULT_SIGNIFICANT_DIGITS;
return DEFAULT_SIGNIFICANT_DIGITS;
}
return precision;
};
@@ -130,6 +121,11 @@ export const getYAxisFormattedValue = (
};
try {
// Use custom formatter for the 'none' format honoring precision
if (format === 'none') {
return formatDecimalWithLeadingZeros(numValue, precision);
}
const formatter = getValueFormat(format);
const formattedValue = formatter(numValue, computeDecimals(), undefined);
if (formattedValue.text && formattedValue.text.includes('.')) {

View File

@@ -471,11 +471,13 @@ function LogsFormatOptionsMenu({
rootClassName="format-options-popover"
destroyTooltipOnHide
>
<Button
className="periscope-btn ghost"
icon={<Sliders size={14} />}
data-testid="periscope-btn-format-options"
/>
<Tooltip title="Options">
<Button
className="periscope-btn ghost"
icon={<Sliders size={14} />}
data-testid="periscope-btn-format-options"
/>
</Tooltip>
</Popover>
);
}

View File

@@ -1,223 +0,0 @@
import { render } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
import { Userpilot } from 'userpilot';
import UserpilotRouteTracker from './UserpilotRouteTracker';
// Mock constants
const INITIAL_PATH = '/initial';
const TIMER_DELAY = 100;
// Mock the userpilot module
jest.mock('userpilot', () => ({
Userpilot: {
reload: jest.fn(),
},
}));
// Mock location state
let mockLocation = {
pathname: INITIAL_PATH,
search: '',
hash: '',
state: null,
};
// Mock react-router-dom
jest.mock('react-router-dom', () => {
const originalModule = jest.requireActual('react-router-dom');
return {
...originalModule,
useLocation: jest.fn(() => mockLocation),
};
});
describe('UserpilotRouteTracker', () => {
beforeEach(() => {
jest.clearAllMocks();
// Reset timers
jest.useFakeTimers();
// Reset error mock implementation
(Userpilot.reload as jest.Mock).mockImplementation(() => {});
// Reset location to initial state
mockLocation = {
pathname: INITIAL_PATH,
search: '',
hash: '',
state: null,
};
});
afterEach(() => {
jest.useRealTimers();
});
it('calls Userpilot.reload on initial render', () => {
render(
<MemoryRouter>
<UserpilotRouteTracker />
</MemoryRouter>,
);
// Fast-forward timer to trigger the setTimeout in reloadUserpilot
act(() => {
jest.advanceTimersByTime(TIMER_DELAY);
});
expect(Userpilot.reload).toHaveBeenCalledTimes(1);
});
it('calls Userpilot.reload when pathname changes', () => {
const { rerender } = render(
<MemoryRouter>
<UserpilotRouteTracker />
</MemoryRouter>,
);
// Fast-forward initial render timer
act(() => {
jest.advanceTimersByTime(TIMER_DELAY);
});
jest.clearAllMocks();
// Create a new location object with different pathname
const newLocation = {
...mockLocation,
pathname: '/new-path',
};
// Update the mock location with new path and trigger re-render
act(() => {
mockLocation = newLocation;
// Force a component update with the new location
rerender(
<MemoryRouter>
<UserpilotRouteTracker />
</MemoryRouter>,
);
});
// Fast-forward timer to allow the setTimeout to execute
act(() => {
jest.advanceTimersByTime(TIMER_DELAY);
});
expect(Userpilot.reload).toHaveBeenCalledTimes(1);
});
it('calls Userpilot.reload when search parameters change', () => {
const { rerender } = render(
<MemoryRouter>
<UserpilotRouteTracker />
</MemoryRouter>,
);
// Fast-forward initial render timer
act(() => {
jest.advanceTimersByTime(TIMER_DELAY);
});
jest.clearAllMocks();
// Create a new location object with different search params
const newLocation = {
...mockLocation,
search: '?param=value',
};
// Update the mock location with new search and trigger re-render
// eslint-disable-next-line sonarjs/no-identical-functions
act(() => {
mockLocation = newLocation;
// Force a component update with the new location
rerender(
<MemoryRouter>
<UserpilotRouteTracker />
</MemoryRouter>,
);
});
// Fast-forward timer to allow the setTimeout to execute
act(() => {
jest.advanceTimersByTime(TIMER_DELAY);
});
expect(Userpilot.reload).toHaveBeenCalledTimes(1);
});
it('handles errors in Userpilot.reload gracefully', () => {
// Mock console.error to prevent test output noise and capture calls
const consoleErrorSpy = jest
.spyOn(console, 'error')
.mockImplementation(() => {});
// Instead of using the component, we test the error handling behavior directly
const errorMsg = 'Error message';
// Set up a function that has the same error handling behavior as in component
const testErrorHandler = (): void => {
try {
if (typeof Userpilot !== 'undefined' && Userpilot.reload) {
Userpilot.reload();
}
} catch (error) {
console.error('[Userpilot] Error reloading on route change:', error);
}
};
// Make Userpilot.reload throw an error
(Userpilot.reload as jest.Mock).mockImplementation(() => {
throw new Error(errorMsg);
});
// Execute the function that should handle errors
testErrorHandler();
// Verify error was logged
expect(consoleErrorSpy).toHaveBeenCalledWith(
'[Userpilot] Error reloading on route change:',
expect.any(Error),
);
// Restore console mock
consoleErrorSpy.mockRestore();
});
it('does not call Userpilot.reload when same route is rendered again', () => {
const { rerender } = render(
<MemoryRouter>
<UserpilotRouteTracker />
</MemoryRouter>,
);
// Fast-forward initial render timer
act(() => {
jest.advanceTimersByTime(TIMER_DELAY);
});
jest.clearAllMocks();
act(() => {
mockLocation = {
pathname: mockLocation.pathname,
search: mockLocation.search,
hash: mockLocation.hash,
state: mockLocation.state,
};
// Force a component update with the same location
rerender(
<MemoryRouter>
<UserpilotRouteTracker />
</MemoryRouter>,
);
});
// Fast-forward timer
act(() => {
jest.advanceTimersByTime(TIMER_DELAY);
});
// Should not call reload since path and search are the same
expect(Userpilot.reload).not.toHaveBeenCalled();
});
});

View File

@@ -1,60 +0,0 @@
import { useCallback, useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import { Userpilot } from 'userpilot';
/**
* UserpilotRouteTracker - A component that tracks route changes and calls Userpilot.reload
* on actual page changes (pathname changes or significant query parameter changes).
*
* This component renders nothing and is designed to be placed once high in the component tree.
*/
function UserpilotRouteTracker(): null {
const location = useLocation();
const prevPathRef = useRef<string>(location.pathname);
const prevSearchRef = useRef<string>(location.search);
const isFirstRenderRef = useRef<boolean>(true);
// Function to reload Userpilot safely - using useCallback to avoid dependency issues
const reloadUserpilot = useCallback((): void => {
try {
if (typeof Userpilot !== 'undefined' && Userpilot.reload) {
setTimeout(() => {
Userpilot.reload();
}, 100);
}
} catch (error) {
console.error('[Userpilot] Error reloading on route change:', error);
}
}, []);
// Handle first render
useEffect(() => {
if (isFirstRenderRef.current) {
isFirstRenderRef.current = false;
reloadUserpilot();
}
}, [reloadUserpilot]);
// Handle route/query changes
useEffect(() => {
// Skip first render as it's handled by the effect above
if (isFirstRenderRef.current) {
return;
}
// Check if the path has changed or if significant query params have changed
const pathChanged = location.pathname !== prevPathRef.current;
const searchChanged = location.search !== prevSearchRef.current;
if (pathChanged || searchChanged) {
// Update refs
prevPathRef.current = location.pathname;
prevSearchRef.current = location.search;
reloadUserpilot();
}
}, [location.pathname, location.search, reloadUserpilot]);
return null;
}
export default UserpilotRouteTracker;

View File

@@ -244,6 +244,10 @@
}
}
}
// Add border-bottom to table cells when pagination is not present
.ant-spin-container:not(:has(.ant-pagination)) .ant-table-cell {
border-bottom: 1px solid var(--bg-slate-500) !important;
}
.endpoints-table-container {
display: flex;
@@ -422,30 +426,28 @@
gap: 8px;
.endpoint-meta-data-pill {
display: flex;
align-items: flex-start;
border-radius: 4px;
border: 1px solid var(--bg-slate-300);
width: fit-content;
overflow: hidden;
box-sizing: content-box;
.endpoint-meta-data-label {
display: flex;
padding: 6px 8px;
align-items: center;
gap: 4px;
border-right: 1px solid var(--bg-slate-300);
color: var(--text-vanilla-100);
font-size: 14px;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
padding: 6px 8px;
background: var(--bg-slate-500);
height: calc(100% - 12px);
}
.endpoint-meta-data-value {
display: flex;
padding: 6px 8px;
justify-content: center;
align-items: center;
gap: 10px;
color: var(--text-vanilla-400);
background: var(--bg-slate-400);
height: calc(100% - 12px);
font-size: 14px;
line-height: 18px;
letter-spacing: -0.07px;
}
}
}
@@ -453,9 +455,23 @@
.endpoint-details-filters-container {
display: flex;
flex-direction: row;
align-items: center;
border: 1px solid var(--bg-slate-500);
height: 36px;
box-sizing: content-box;
.ant-select-selector {
border: none !important;
}
.endpoint-details-filters-container-dropdown {
width: 120px;
border-right: 1px solid var(--bg-slate-500);
height: 36px;
display: flex;
align-items: center;
.ant-select-single {
height: 32px;
}
}
.endpoint-details-filters-container-search {
@@ -996,7 +1012,6 @@
.lightMode {
.ant-drawer-header {
border-bottom: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
}
@@ -1007,6 +1022,25 @@
}
.domain-detail-drawer {
.endpoint-details-card,
.status-code-table-container,
.endpoint-details-filters-container,
.endpoint-details-filters-container-dropdown,
.ant-radio-button-wrapper,
.views-tabs-container,
.ant-btn-default.tab,
.tab::before,
.endpoint-meta-data-pill,
.endpoint-meta-data-label,
.endpoints-table-container,
.group-by-label,
.ant-select-selector,
.ant-drawer-header {
border-color: var(--bg-vanilla-300) !important;
}
.views-tabs .tab::before {
background: var(--bg-vanilla-300);
}
.title {
color: var(--text-ink-300);
}
@@ -1031,7 +1065,6 @@
.selected_view {
background: var(--bg-vanilla-300);
border: 1px solid var(--bg-slate-300);
color: var(--text-ink-400);
}
@@ -1160,7 +1193,11 @@
}
}
.top-services-content {
border-color: var(--bg-vanilla-300);
}
.dependent-services-container {
border: none;
padding: 10px 12px;
.top-services-item {
display: flex;
@@ -1187,11 +1224,31 @@
}
.top-services-item-progress-bar {
background-color: var(--bg-vanilla-300);
border: 1px solid var(--bg-slate-300);
background-color: var(--bg-vanilla-200);
border: 1px solid var(--bg-vanilla-300);
}
}
}
.ant-table {
.ant-table-thead > tr > th {
color: var(--text-ink-300);
}
.ant-table-cell {
&,
&:has(.top-services-item-latency) {
background: var(--bg-vanilla-100);
}
color: var(--text-ink-300);
}
.ant-table-tbody > tr:hover > td {
background: var(--bg-vanilla-200);
}
.table-row-dark {
background: var(--bg-vanilla-300);
}
}
.top-services-item-percentage {
color: var(--text-ink-300);
@@ -1225,4 +1282,8 @@
}
}
}
// Add border-bottom to table cells when pagination is not present
.ant-spin-container:not(:has(.ant-pagination)) .ant-table-cell {
border-bottom: 1px solid var(--bg-vanilla-300) !important;
}
}

View File

@@ -57,7 +57,8 @@ describe('Request AWS integration', () => {
expect(capturedPayload.attributes).toEqual({
screen: 'AWS integration details',
integration: 's3 sync',
tenant_url: 'localhost',
deployment_url: 'localhost',
user_email: null,
});
});
});

View File

@@ -1,8 +1,9 @@
import { Select, Tooltip, Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Info } from 'lucide-react';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { ALL_SELECTED_VALUE } from '../constants';
import { useCreateAlertState } from '../context';
function MultipleNotifications(): JSX.Element {
@@ -12,6 +13,12 @@ function MultipleNotifications(): JSX.Element {
} = useCreateAlertState();
const { currentQuery } = useQueryBuilder();
const isAllOptionSelected = useMemo(
() =>
notificationSettings.multipleNotifications?.includes(ALL_SELECTED_VALUE),
[notificationSettings.multipleNotifications],
);
const spaceAggregationOptions = useMemo(() => {
const allGroupBys = currentQuery.builder.queryData?.reduce<string[]>(
(acc, query) => {
@@ -21,15 +28,60 @@ function MultipleNotifications(): JSX.Element {
[],
);
const uniqueGroupBys = [...new Set(allGroupBys)];
return uniqueGroupBys.map((key) => ({
const options = uniqueGroupBys.map((key) => ({
label: key,
value: key,
disabled: isAllOptionSelected,
'data-testid': 'multiple-notifications-select-option',
}));
}, [currentQuery.builder.queryData]);
if (options.length > 0) {
return [
{
label: 'All',
value: ALL_SELECTED_VALUE,
'data-testid': 'multiple-notifications-select-option',
},
...options,
];
}
return options;
}, [currentQuery.builder.queryData, isAllOptionSelected]);
const isMultipleNotificationsEnabled = spaceAggregationOptions.length > 0;
const onSelectChange = useCallback(
(newSelectedOptions: string[]): void => {
const currentSelectedOptions = notificationSettings.multipleNotifications;
const allOptionLastSelected =
!currentSelectedOptions?.includes(ALL_SELECTED_VALUE) &&
newSelectedOptions.includes(ALL_SELECTED_VALUE);
if (allOptionLastSelected) {
setNotificationSettings({
type: 'SET_MULTIPLE_NOTIFICATIONS',
payload: [ALL_SELECTED_VALUE],
});
} else {
setNotificationSettings({
type: 'SET_MULTIPLE_NOTIFICATIONS',
payload: newSelectedOptions,
});
}
},
[setNotificationSettings, notificationSettings.multipleNotifications],
);
const groupByDescription = useMemo(() => {
if (isAllOptionSelected) {
return 'All = grouping of alerts is disabled';
}
if (notificationSettings.multipleNotifications?.length) {
return `Alerts with same ${notificationSettings.multipleNotifications?.join(
', ',
)} will be grouped`;
}
return 'Empty = all matching alerts combined into one notification';
}, [isAllOptionSelected, notificationSettings.multipleNotifications]);
const multipleNotificationsInput = useMemo(() => {
const placeholder = isMultipleNotificationsEnabled
? 'Select fields to group by (optional)'
@@ -38,12 +90,7 @@ function MultipleNotifications(): JSX.Element {
<div>
<Select
options={spaceAggregationOptions}
onChange={(value): void => {
setNotificationSettings({
type: 'SET_MULTIPLE_NOTIFICATIONS',
payload: value,
});
}}
onChange={onSelectChange}
value={notificationSettings.multipleNotifications}
mode="multiple"
placeholder={placeholder}
@@ -54,11 +101,7 @@ function MultipleNotifications(): JSX.Element {
/>
{isMultipleNotificationsEnabled && (
<Typography.Paragraph className="multiple-notifications-select-description">
{notificationSettings.multipleNotifications?.length
? `Alerts with same ${notificationSettings.multipleNotifications?.join(
', ',
)} will be grouped`
: 'Empty = all matching alerts combined into one notification'}
{groupByDescription}
</Typography.Paragraph>
)}
</div>
@@ -72,9 +115,10 @@ function MultipleNotifications(): JSX.Element {
}
return input;
}, [
groupByDescription,
isMultipleNotificationsEnabled,
notificationSettings.multipleNotifications,
setNotificationSettings,
onSelectChange,
spaceAggregationOptions,
]);

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ALL_SELECTED_VALUE } from 'container/CreateAlertV2/constants';
import * as createAlertContext from 'container/CreateAlertV2/context';
import {
INITIAL_ALERT_THRESHOLD_STATE,
@@ -28,6 +29,9 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
}));
const TEST_QUERY = 'test-query';
const TEST_QUERY_2 = 'test-query-2';
const ANT_SELECT_ITEM_OPTION_CONTENT_SELECTOR =
'.ant-select-item-option-content';
const TEST_GROUP_BY_FIELDS = [{ key: 'service' }, { key: 'environment' }];
const TRUE = 'true';
const FALSE = 'false';
@@ -151,7 +155,7 @@ describe('MultipleNotifications', () => {
groupBy: [{ key: 'http.status_code' }],
},
{
queryName: 'test-query-2',
queryName: TEST_QUERY_2,
groupBy: [{ key: 'service' }],
},
],
@@ -165,8 +169,121 @@ describe('MultipleNotifications', () => {
await userEvent.click(select);
expect(
screen.getByRole('option', { name: 'http.status_code' }),
screen.getByText('http.status_code', {
selector: ANT_SELECT_ITEM_OPTION_CONTENT_SELECTOR,
}),
).toBeInTheDocument();
expect(screen.getByRole('option', { name: 'service' })).toBeInTheDocument();
expect(
screen.getByText('service', {
selector: ANT_SELECT_ITEM_OPTION_CONTENT_SELECTOR,
}),
).toBeInTheDocument();
expect(
screen.getByText('All', {
selector: ANT_SELECT_ITEM_OPTION_CONTENT_SELECTOR,
}),
).toBeInTheDocument();
});
it('selecting the "all" option shows correct group by description', () => {
useQueryBuilder.mockReturnValue({
currentQuery: {
builder: {
queryData: [
{
queryName: TEST_QUERY_2,
groupBy: [{ key: 'service' }],
},
],
},
},
});
jest.spyOn(createAlertContext, 'useCreateAlertState').mockReturnValue(
createMockAlertContextState({
notificationSettings: {
...INITIAL_NOTIFICATION_SETTINGS_STATE,
multipleNotifications: [ALL_SELECTED_VALUE],
},
}),
);
render(<MultipleNotifications />);
expect(
screen.getByText('All = grouping of alerts is disabled'),
).toBeInTheDocument();
});
it('selecting "all" option should disable selection of other options', async () => {
useQueryBuilder.mockReturnValue({
currentQuery: {
builder: {
queryData: [
{
queryName: TEST_QUERY_2,
groupBy: [{ key: 'service' }],
},
],
},
},
});
jest.spyOn(createAlertContext, 'useCreateAlertState').mockReturnValue(
createMockAlertContextState({
notificationSettings: {
...INITIAL_NOTIFICATION_SETTINGS_STATE,
multipleNotifications: [ALL_SELECTED_VALUE],
},
}),
);
render(<MultipleNotifications />);
const select = screen.getByRole(COMBOBOX_ROLE);
await userEvent.click(select);
const serviceOption = screen.getAllByTestId(
'multiple-notifications-select-option',
);
expect(serviceOption).toHaveLength(2);
expect(serviceOption[0]).not.toHaveClass('ant-select-item-option-disabled');
expect(serviceOption[1]).toHaveClass('ant-select-item-option-disabled');
});
it('selecting all option should remove all other selected options', async () => {
useQueryBuilder.mockReturnValue({
currentQuery: {
builder: {
queryData: [
{
queryName: TEST_QUERY_2,
groupBy: [{ key: 'service' }],
},
],
},
},
});
jest.spyOn(createAlertContext, 'useCreateAlertState').mockReturnValue(
createMockAlertContextState({
notificationSettings: {
...INITIAL_NOTIFICATION_SETTINGS_STATE,
multipleNotifications: ['service', 'environment'],
},
setNotificationSettings: mockSetNotificationSettings,
}),
);
render(<MultipleNotifications />);
const select = screen.getByRole(COMBOBOX_ROLE);
await userEvent.click(select);
const serviceOption = screen.getAllByTestId(
'multiple-notifications-select-option',
);
expect(serviceOption).toHaveLength(2);
await userEvent.click(serviceOption[0]);
expect(mockSetNotificationSettings).toHaveBeenCalledWith({
type: 'SET_MULTIPLE_NOTIFICATIONS',
payload: [ALL_SELECTED_VALUE],
});
});
});

View File

@@ -72,3 +72,5 @@ export const defaultPostableAlertRuleV2: PostableAlertRuleV2 = {
alert: 'TEST_ALERT',
evaluation: defaultEvaluation,
};
export const ALL_SELECTED_VALUE = '__all__';

View File

@@ -274,6 +274,7 @@ function Login(): JSX.Element {
autoFocus
disabled={versionLoading}
className="login-form-input"
onPressEnter={onNextHandler}
/>
</FormContainer.Item>
</ParentContainer>

View File

@@ -6,6 +6,8 @@
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
overflow: hidden;
text-overflow: ellipsis;
}
.dot {
@@ -25,16 +27,21 @@
.left-section {
display: flex;
align-items: center;
width: 90%;
flex: 1;
min-width: 0;
gap: 8px;
.value {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.right-section {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
.data-type {
display: flex;
@@ -45,6 +52,7 @@
gap: 4px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.08);
white-space: nowrap;
}
.type-tag {
@@ -56,6 +64,7 @@
gap: 4px;
border-radius: 50px;
text-transform: capitalize;
white-space: nowrap;
&.tag {
border-radius: 50px;

View File

@@ -451,6 +451,12 @@ export const getUPlotChartOptions = ({
(self): void => {
const selection = self.select;
if (selection) {
// Cleanup any visible "View Traces" buttons when drag selection occurs
const activeButtons = document.querySelectorAll(
'.view-onclick-show-button',
);
activeButtons.forEach((btn) => btn.remove());
const startTime = self.posToVal(selection.left, 'x');
const endTime = self.posToVal(selection.left + selection.width, 'x');

View File

@@ -1,4 +1,5 @@
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import listOrgPreferences from 'api/v1/org/preferences/list';
import get from 'api/v1/user/me/get';
import listUserPreferences from 'api/v1/user/preferences/list';
@@ -77,6 +78,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
useEffect(() => {
if (!isFetchingUser && userData && userData.data) {
setLocalStorageApi(LOCALSTORAGE.LOGGED_IN_USER_EMAIL, userData.data.email);
setUser((prev) => ({
...prev,
...userData.data,

View File

@@ -23,7 +23,6 @@ const plugins = [
PYLON_APP_ID: process.env.PYLON_APP_ID,
APPCUES_APP_ID: process.env.APPCUES_APP_ID,
POSTHOG_KEY: process.env.POSTHOG_KEY,
USERPILOT_KEY: process.env.USERPILOT_KEY,
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
SENTRY_ORG: process.env.SENTRY_ORG,
SENTRY_PROJECT_ID: process.env.SENTRY_PROJECT_ID,
@@ -40,9 +39,9 @@ const plugins = [
FRONTEND_API_ENDPOINT: process.env.FRONTEND_API_ENDPOINT,
WEBSOCKET_API_ENDPOINT: process.env.WEBSOCKET_API_ENDPOINT,
PYLON_APP_ID: process.env.PYLON_APP_ID,
PYLON_IDENTITY_SECRET: process.env.PYLON_IDENTITY_SECRET,
APPCUES_APP_ID: process.env.APPCUES_APP_ID,
POSTHOG_KEY: process.env.POSTHOG_KEY,
USERPILOT_KEY: process.env.USERPILOT_KEY,
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
SENTRY_ORG: process.env.SENTRY_ORG,
SENTRY_PROJECT_ID: process.env.SENTRY_PROJECT_ID,

View File

@@ -28,7 +28,6 @@ const plugins = [
PYLON_APP_ID: process.env.PYLON_APP_ID,
APPCUES_APP_ID: process.env.APPCUES_APP_ID,
POSTHOG_KEY: process.env.POSTHOG_KEY,
USERPILOT_KEY: process.env.USERPILOT_KEY,
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
SENTRY_ORG: process.env.SENTRY_ORG,
SENTRY_PROJECT_ID: process.env.SENTRY_PROJECT_ID,
@@ -50,9 +49,9 @@ const plugins = [
FRONTEND_API_ENDPOINT: process.env.FRONTEND_API_ENDPOINT,
WEBSOCKET_API_ENDPOINT: process.env.WEBSOCKET_API_ENDPOINT,
PYLON_APP_ID: process.env.PYLON_APP_ID,
PYLON_IDENTITY_SECRET: process.env.PYLON_IDENTITY_SECRET,
APPCUES_APP_ID: process.env.APPCUES_APP_ID,
POSTHOG_KEY: process.env.POSTHOG_KEY,
USERPILOT_KEY: process.env.USERPILOT_KEY,
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
SENTRY_ORG: process.env.SENTRY_ORG,
SENTRY_PROJECT_ID: process.env.SENTRY_PROJECT_ID,

View File

@@ -3385,30 +3385,6 @@
strict-event-emitter "^0.2.4"
web-encoding "^1.1.5"
"@ndhoule/each@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@ndhoule/each/-/each-2.0.1.tgz#bbed372a603e0713a3193c706a73ddebc5b426a9"
integrity sha512-wHuJw6x+rF6Q9Skgra++KccjBozCr9ymtna0FhxmV/8xT/hZ2ExGYR8SV8prg8x4AH/7mzDYErNGIVHuzHeybw==
dependencies:
"@ndhoule/keys" "^2.0.0"
"@ndhoule/includes@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@ndhoule/includes/-/includes-2.0.1.tgz#051ff5eb042c8fa17e7158f0a8a70172e1affaa5"
integrity sha512-Q8zN6f3yIhxgBwZ5ldLozHqJlc/fRQ5+hFFsPMFeC9SJvz0nq8vG9hoRXL1c1iaNFQd7yAZIy2igQpERoFqxqg==
dependencies:
"@ndhoule/each" "^2.0.1"
"@ndhoule/keys@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@ndhoule/keys/-/keys-2.0.0.tgz#3d64ae677c65a261747bf3a457c62eb292a4e0ce"
integrity sha512-vtCqKBC1Av6dsBA8xpAO+cgk051nfaI+PnmTZep2Px0vYrDvpUmLxv7z40COlWH5yCpu3gzNhepk+02yiQiZNw==
"@ndhoule/pick@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@ndhoule/pick/-/pick-2.0.0.tgz#e1eb1a6ca3243eef56daa095c3a1612c74a52156"
integrity sha512-xkYtpf1pRd8egwvl5tJcdGu+GBd6ZZH3S/zoIQ9txEI+pHF9oTIlxMC9G4CB3sRugAeLgu8qYJGl3tnxWq74Qw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
@@ -3529,18 +3505,25 @@
resolved "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.8.0.tgz"
integrity sha512-AhVAm6SQ+zgxIiOzwVdUcDmKlu/qU39FiYD2UD6kQQaVenrn0dGZewIghWAENGQsvC+1avLCuT+T2/3Gsp/W3w==
"@playwright/test@1.54.1":
version "1.54.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.54.1.tgz#a76333e5c2cba5f12f96a6da978bba3ab073c7e6"
integrity sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==
"@playwright/test@1.55.1":
version "1.55.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.55.1.tgz#80f775d5f948cd3ef550fcc45ef99986d3ffb36c"
integrity sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==
dependencies:
playwright "1.54.1"
playwright "1.55.1"
"@polka/url@^1.0.0-next.20":
version "1.0.0-next.21"
resolved "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz"
integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==
"@posthog/core@1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@posthog/core/-/core-1.6.0.tgz#a5b63a30950a8dfe87d4bf335ab24005c7ce1278"
integrity sha512-Tbh8UACwbb7jFdDC7wwXHtfNzO+4wKh3VbyMHmp2UBe6w1jliJixexTJNfkqdGZm+ht3M10mcKvGGPnoZ2zLBg==
dependencies:
cross-spawn "^7.0.6"
"@radix-ui/primitive@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.1.tgz#e46f9958b35d10e9f6dc71c497305c22e3e55dbd"
@@ -4685,6 +4668,11 @@
tapable "^2.0.0"
webpack "^5.1.0"
"@types/crypto-js@4.2.2":
version "4.2.2"
resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea"
integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==
"@types/d3-array@3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.3.tgz#87d990bf504d14ad6b16766979d04e943c046dac"
@@ -7518,11 +7506,6 @@ compare-func@^2.0.0:
array-ify "^1.0.0"
dot-prop "^5.1.0"
component-indexof@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/component-indexof/-/component-indexof-0.0.3.tgz#11d091312239eb8f32c8f25ae9cb002ffe8d3c24"
integrity sha512-puDQKvx/64HZXb4hBwIcvQLaLgux8o1CbWl39s41hrIIZDl1lJiD5jc22gj3RBeGK0ovxALDYpIbyjqDUUl0rw==
compressible@~2.0.16:
version "2.0.18"
resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz"
@@ -7733,7 +7716,7 @@ cross-fetch@3.1.5:
dependencies:
node-fetch "2.6.7"
cross-spawn@7.0.5, cross-spawn@^6.0.5, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
cross-spawn@7.0.5, cross-spawn@^6.0.5, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6:
version "7.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82"
integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==
@@ -7742,6 +7725,11 @@ cross-spawn@7.0.5, cross-spawn@^6.0.5, cross-spawn@^7.0.1, cross-spawn@^7.0.2, c
shebang-command "^2.0.0"
which "^2.0.1"
crypto-js@4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
css-box-model@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
@@ -11241,11 +11229,6 @@ is-wsl@^3.1.0:
dependencies:
is-inside-container "^1.0.0"
is@^3.1.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/is/-/is-3.3.0.tgz#61cff6dd3c4193db94a3d62582072b44e5645d79"
integrity sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
@@ -13616,11 +13599,6 @@ nwsapi@^2.2.0:
resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.4.tgz"
integrity sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==
obj-case@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/obj-case/-/obj-case-0.2.1.tgz#13a554d04e5ca32dfd9d566451fd2b0e11007f1a"
integrity sha512-PquYBBTy+Y6Ob/O2574XHhDtHJlV1cJHMCgW+rDRc9J5hhmRelJB3k5dTK/3cVmFVtzvAKuENeuLpoyTzMzkOg==
object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"
@@ -14197,17 +14175,17 @@ pkg-dir@^7.0.0:
dependencies:
find-up "^6.3.0"
playwright-core@1.54.1:
version "1.54.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.54.1.tgz#d32edcce048c9d83ceac31e294a7b60ef586960b"
integrity sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==
playwright-core@1.55.1:
version "1.55.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.55.1.tgz#5d3bb1846bc4289d364ea1a9dcb33f14545802e9"
integrity sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==
playwright@1.54.1:
version "1.54.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.54.1.tgz#128d66a8d5182b5330e6440be3a72ca313362788"
integrity sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==
playwright@1.55.1:
version "1.55.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.55.1.tgz#8a9954e9e61ed1ab479212af9be336888f8b3f0e"
integrity sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==
dependencies:
playwright-core "1.54.1"
playwright-core "1.55.1"
optionalDependencies:
fsevents "2.3.2"
@@ -14530,15 +14508,16 @@ postcss@^8.4.35:
picocolors "^1.1.1"
source-map-js "^1.2.1"
posthog-js@1.215.5:
version "1.215.5"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.215.5.tgz#0512cfdb919da960b809c5f686ca147f9c2922ba"
integrity sha512-42lPur+xvkp51pHz2FQ7Y+KHdZ4eQSNIhUO03EECvc2UsmnM0FiVTrF1bcLwHZMaWfR26gOeuOAAjTUV9tinJg==
posthog-js@1.298.0:
version "1.298.0"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.298.0.tgz#54730988a753220aef54af0c4e69960633917450"
integrity sha512-Zwzsf7TO8qJ6DFLuUlQSsT/5OIOcxSBZlKOSk3satkEnwKdmnBXUuxgVXRHrvq1kj7OB2PVAPgZiQ8iHHj9DRA==
dependencies:
"@posthog/core" "1.6.0"
core-js "^3.38.1"
fflate "^0.4.8"
preact "^10.19.3"
web-vitals "^4.2.0"
web-vitals "^4.2.4"
preact@^10.19.3:
version "10.22.0"
@@ -17907,17 +17886,6 @@ use-sync-external-store@^1.0.0:
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
userpilot@1.3.9:
version "1.3.9"
resolved "https://registry.yarnpkg.com/userpilot/-/userpilot-1.3.9.tgz#6374083f3e84cbf1fc825133588b5b499054271b"
integrity sha512-V0QIuIlAJPB8s3j+qtv7BW7NKSXthlZWuowIu+IZOMGLgUbqQTaSW5m1Ct4wJviPKUNOi8kbhCXN4c4b3zcJzg==
dependencies:
"@ndhoule/includes" "^2.0.1"
"@ndhoule/pick" "^2.0.0"
component-indexof "0.0.3"
is "^3.1.0"
obj-case "^0.2.0"
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
@@ -18124,7 +18092,7 @@ web-vitals@^0.2.4:
resolved "https://registry.npmjs.org/web-vitals/-/web-vitals-0.2.4.tgz"
integrity sha512-6BjspCO9VriYy12z356nL6JBS0GYeEcA457YyRzD+dD6XYCQ75NKhcOHUMHentOE7OcVCIXXDvOm0jKFfQG2Gg==
web-vitals@^4.2.0:
web-vitals@^4.2.4:
version "4.2.4"
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.4.tgz#1d20bc8590a37769bd0902b289550936069184b7"
integrity sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==

View File

@@ -1293,7 +1293,12 @@ func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, params
coldStorageDuration = int(params.ToColdStorageDuration)
}
tableNameArray := []string{r.logsDB + "." + r.logsLocalTableV2, r.logsDB + "." + r.logsResourceLocalTableV2}
tableNameArray := []string{
r.logsDB + "." + r.logsLocalTableV2,
r.logsDB + "." + r.logsResourceLocalTableV2,
getLocalTableName(r.logsDB + "." + r.logsAttributeKeys),
getLocalTableName(r.logsDB + "." + r.logsResourceKeys),
}
// check if there is existing things to be done
for _, tableName := range tableNameArray {
@@ -1327,9 +1332,19 @@ func (r *ClickHouseReader) setTTLLogs(ctx context.Context, orgID string, params
params.ToColdStorageDuration, params.ColdStorageVolume)
}
ttlLogsV2AttributeKeys := fmt.Sprintf(
"ALTER TABLE %v ON CLUSTER %s MODIFY TTL timestamp + "+
"INTERVAL %v SECOND DELETE", tableNameArray[2], r.cluster, params.DelDuration)
ttlLogsV2ResourceKeys := fmt.Sprintf(
"ALTER TABLE %v ON CLUSTER %s MODIFY TTL timestamp + "+
"INTERVAL %v SECOND DELETE", tableNameArray[3], r.cluster, params.DelDuration)
ttlPayload := map[string]string{
tableNameArray[0]: ttlLogsV2,
tableNameArray[1]: ttlLogsV2Resource,
tableNameArray[2]: ttlLogsV2AttributeKeys,
tableNameArray[3]: ttlLogsV2ResourceKeys,
}
// set the ttl if nothing is pending/ no errors
@@ -1435,6 +1450,7 @@ func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, param
r.TraceDB + "." + signozUsageExplorerTable,
r.TraceDB + "." + defaultDependencyGraphTable,
r.TraceDB + "." + r.traceSummaryTable,
r.TraceDB + "." + r.spanAttributesKeysTable,
}
coldStorageDuration := -1
@@ -1502,7 +1518,7 @@ func (r *ClickHouseReader) setTTLTraces(ctx context.Context, orgID string, param
req = fmt.Sprintf(ttlV2Resource, tableName, r.cluster, params.DelDuration)
}
if len(params.ColdStorageVolume) > 0 {
if len(params.ColdStorageVolume) > 0 && !strings.HasSuffix(distributedTableName, r.spanAttributesKeysTable) {
if strings.HasSuffix(distributedTableName, r.traceResourceTableV3) {
req += fmt.Sprintf(ttlTracesV2ResourceColdStorage, params.ToColdStorageDuration, params.ColdStorageVolume)
} else {
@@ -1649,6 +1665,8 @@ func (r *ClickHouseReader) SetTTLV2(ctx context.Context, orgID string, params *m
tableNames := []string{
r.logsDB + "." + r.logsLocalTableV2,
r.logsDB + "." + r.logsResourceLocalTableV2,
getLocalTableName(r.logsDB + "." + r.logsAttributeKeys),
getLocalTableName(r.logsDB + "." + r.logsResourceKeys),
}
for _, tableName := range tableNames {
@@ -1696,6 +1714,23 @@ func (r *ClickHouseReader) SetTTLV2(ctx context.Context, orgID string, params *m
ttlPayload[tableNames[1]] = resourceQueries
// NOTE: Since logs support custom rule based retention, that makes it difficult to identify which attributes, resource keys
// we need to keep, hence choosing MAX for safe side and not to create any complex solution for this.
maxRetentionTTL := params.DefaultTTLDays
for _, rule := range params.TTLConditions {
maxRetentionTTL = max(maxRetentionTTL, rule.TTLDays)
}
ttlPayload[tableNames[2]] = []string{
fmt.Sprintf("ALTER TABLE %s ON CLUSTER %s MODIFY TTL timestamp + toIntervalDay(%d) DELETE SETTINGS materialize_ttl_after_modify=0",
tableNames[2], r.cluster, maxRetentionTTL),
}
ttlPayload[tableNames[3]] = []string{
fmt.Sprintf("ALTER TABLE %s ON CLUSTER %s MODIFY TTL timestamp + toIntervalDay(%d) DELETE SETTINGS materialize_ttl_after_modify=0",
tableNames[3], r.cluster, maxRetentionTTL),
}
ttlConditionsJSON, err := json.Marshal(params.TTLConditions)
if err != nil {
return nil, errorsV2.Wrapf(err, errorsV2.TypeInternal, errorsV2.CodeInternal, "error marshalling TTL condition")

View File

@@ -2000,9 +2000,8 @@ func (aH *APIHandler) getTTL(w http.ResponseWriter, r *http.Request) {
render.Error(w, err)
return
}
result, err := aH.reader.GetTTL(r.Context(), claims.OrgID, ttlParams)
if err != nil {
render.Error(w, err)
result, apiErr := aH.reader.GetTTL(r.Context(), claims.OrgID, ttlParams)
if apiErr != nil && aH.HandleError(w, apiErr.Err, http.StatusInternalServerError) {
return
}
aH.WriteJSON(w, r, result)

View File

@@ -60,6 +60,6 @@ def pytest_addoption(parser: pytest.Parser):
parser.addoption(
"--schema-migrator-version",
action="store",
default="v0.129.6",
default="v0.129.7",
help="schema migrator version",
)

View File

@@ -37,7 +37,7 @@ def test_set_ttl_traces_success(
}
headers = {
"Authorization": f"Bearer {get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
"Authorization": f"Bearer {get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)}"
}
response = requests.post(
@@ -64,6 +64,7 @@ def test_set_ttl_traces_success(
"usage_explorer",
"dependency_graph_minutes_v2",
"trace_summary",
"span_attributes_keys",
]
# Query to get table engine info which includes TTL
@@ -92,7 +93,7 @@ def test_set_ttl_traces_with_cold_storage(signoz: types.SigNoz, get_token: Calla
}
headers = {
"Authorization": f"Bearer {get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
"Authorization": f"Bearer {get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)}"
}
response = requests.post(
@@ -120,7 +121,7 @@ def test_set_ttl_metrics_success(
}
headers = {
"Authorization": f"Bearer {get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
"Authorization": f"Bearer {get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)}"
}
response = requests.post(
@@ -179,7 +180,7 @@ def test_set_ttl_metrics_with_cold_storage(
}
headers = {
"Authorization": f"Bearer {get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
"Authorization": f"Bearer {get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)}"
}
response = requests.post(
@@ -207,7 +208,7 @@ def test_set_ttl_invalid_type(
}
headers = {
"Authorization": f"Bearer {get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
"Authorization": f"Bearer {get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)}"
}
response = requests.post(
@@ -233,7 +234,7 @@ def test_set_custom_retention_ttl_basic(
}
headers = {
"Authorization": f"Bearer {get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
"Authorization": f"Bearer {get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)}"
}
response = requests.post(
@@ -291,6 +292,26 @@ def test_set_custom_retention_ttl_basic(
retention_col[3] == "100"
), f"Expected default value of _retention_days to be 100 in table {table}, but got {retention_col[3]}"
tables_to_check = [
"logs_attribute_keys",
"logs_resource_keys"
]
# Query to get table engine info which includes TTL
table_list = "', '".join(tables_to_check)
query = f"SELECT engine_full FROM system.tables WHERE table in ['{table_list}']"
result = signoz.telemetrystore.conn.query(query).result_rows
# Verify TTL exists in all table definitions
assert all("TTL" in r[0] for r in result)
assert all(" SETTINGS" in r[0] for r in result)
ttl_parts = [r[0].split("TTL ")[1].split(" SETTINGS")[0] for r in result]
# Also verify the TTL parts contain retention_days
assert all("toIntervalDay(100)" in ttl_part for ttl_part in ttl_parts)
def test_set_custom_retention_ttl_basic_fallback(
signoz: types.SigNoz,
@@ -309,7 +330,7 @@ def test_set_custom_retention_ttl_basic_fallback(
}
headers = {
"Authorization": f"Bearer {get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
"Authorization": f"Bearer {get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)}"
}
response = requests.post(
@@ -332,6 +353,8 @@ def test_set_custom_retention_ttl_basic_fallback(
tables_to_check = [
"logs_v2",
"logs_v2_resource",
"logs_attribute_keys",
"logs_resource_keys"
]
# Query to get table engine info which includes TTL
@@ -364,7 +387,7 @@ def test_set_custom_retention_ttl_basic_101_times(signoz: types.SigNoz, get_toke
}
headers = {
"Authorization": f"Bearer {get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
"Authorization": f"Bearer {get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)}"
}
response = requests.post(
@@ -440,7 +463,7 @@ def test_set_custom_retention_ttl_with_conditions(
}
headers = {
"Authorization": f"Bearer {get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
"Authorization": f"Bearer {get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)}"
}
response = requests.post(
@@ -494,7 +517,7 @@ def test_set_custom_retention_ttl_with_cold_storage(
insert_logs(logs)
headers = {
"Authorization": f"Bearer {get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
"Authorization": f"Bearer {get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)}"
}
response = requests.post(
@@ -538,7 +561,7 @@ def test_set_custom_retention_ttl_duplicate_conditions(
}
headers = {
"Authorization": f"Bearer {get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
"Authorization": f"Bearer {get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)}"
}
response = requests.post(
@@ -575,7 +598,7 @@ def test_set_custom_retention_ttl_invalid_condition(
}
headers = {
"Authorization": f"Bearer {get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
"Authorization": f"Bearer {get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)}"
}
response = requests.post(
@@ -614,7 +637,7 @@ def test_get_custom_retention_ttl(
insert_logs(logs)
headers = {
"Authorization": f"Bearer {get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
"Authorization": f"Bearer {get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)}"
}
set_response = requests.post(
signoz.self.host_configs["8080"].get("/api/v2/settings/ttl"),
@@ -629,7 +652,7 @@ def test_get_custom_retention_ttl(
# Now get the TTL configuration
headers = {
"Authorization": f"Bearer {get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
"Authorization": f"Bearer {get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)}"
}
get_response = requests.get(
@@ -666,7 +689,7 @@ def test_set_ttl_logs_success(
}
headers = {
"Authorization": f"Bearer {get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
"Authorization": f"Bearer {get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)}"
}
response = requests.post(
@@ -683,7 +706,7 @@ def test_set_ttl_logs_success(
# Verify TTL settings in Clickhouse
# Allow some time for the TTL to be applied
time.sleep(2)
time.sleep(5)
# Check TTL settings on relevant logs tables
tables_to_check = [
@@ -719,7 +742,7 @@ def test_get_ttl_traces_success(
}
headers = {
"Authorization": f"Bearer {get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
"Authorization": f"Bearer {get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)}"
}
set_response = requests.post(
@@ -786,7 +809,7 @@ def test_large_ttl_conditions_list(
}
headers = {
"Authorization": f"Bearer {get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)}"
"Authorization": f"Bearer {get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)}"
}
response = requests.post(